{
  "name": "Break a project into steps",
  "nodes": [
    {
      "parameters": {
        "content": "## Break a project into steps\nTurn a project description into a short list of steps with one job each, put them in an order a person can follow, say how long each takes and who does it, and check the plan against the date.\n\n**How to use**\n1. Open **Chat model** and pick your own credential. To use another AI service, delete that node, add the service's chat model node, and connect it to the Model plug under every step and AI check.\n2. Open **Inputs** and paste your material into each field. Examples are filled in so you can try it first.\n3. Click **Execute workflow**.\n4. Open **Result** to read the answer, anything flagged, and the checks left for you.\n\n**Checks**\n- Check 1 (a plain rule, run automatically with no AI): Every quote in the Quote column appears word for word in the project. 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 something written in \"One job\" and \"Done when\" (a blank or a dash does not count). If it fails: try once more, then flag.\n- Check 3 (a plain rule, run automatically with no AI): Every row of the table has something written in \"Needs first\" (a blank or a dash does not count). If it fails: try once more, then flag.\n- Check 4 (a plain rule, run automatically with no AI): Every row of the table has one of \"under an hour\", \"half a day\", \"a day\", \"a few days\" or \"a week or more\" in the Time needed column. If it fails: try once more, then flag.\n- Check 5 (a plain rule, run automatically with no AI): The answer includes the words \"Check the order and the times with the people who will do the work before you start.\". If it fails: try once more, then flag.\n- Check 6 (a plain rule, run automatically with no AI): Every number in the answer also appears in the project. If it fails: try once more, then flag.\n- Check 7 (a second AI prompt answers pass or fail): Read the \"One job\" column in the answer. Does any step name two jobs, joined by \"and\", \"then\", a comma or a list? Answer FAIL and quote each step that does. If it fails: try once more, then flag.\n- Check 8 (a second AI prompt answers pass or fail): Read the table from top to bottom. Does any step come before a step listed in its \"Needs first\" cell, or name a letter that is not a step in the table? Answer FAIL and name each step that does. If it fails: try once more, then flag.\n- Check 9 (a second AI prompt answers pass or fail): Read the \"Does it fit?\" section. Does it say a plan fits or does not fit using only the hours and the date in the project description? If the description gives no hours or no date, does it say so instead of guessing? Answer FAIL and quote each claim the description does not support. If it fails: try once more, then flag.\n- Check 10 (you check it by hand): Read the plan with the person who will do the work. Ask whether any step must come earlier or later than shown and whether each time matches how long it takes them. Only they know how long their own work takes. 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": "5f62c4ca-2551-48c7-a410-0e54a53290e1",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -860
      ]
    },
    {
      "parameters": {},
      "id": "070d72ee-e2d4-4a77-a75b-a8d05e45e0e9",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "2257d884-0764-41d9-a00e-dbb6bbb90263",
              "name": "project",
              "value": "Sample: We need to run a one-day training on the new case-tracking system for 14 staff. It is on Thursday, November 19, 9 am to 3 pm, in the main office. The trainer, Hana, is free that day only. We need a room with a projector, a practice copy of the system filled with made-up clients, and a one-page quick guide for each person. Staff must hear about it at least a week ahead. Lunch is provided. I have about 5 hours a week to work on this, and Omar in operations can help with the room and lunch.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "37a97b63-d619-43c0-ae51-a779073eaf9e",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: project = The project.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful assistant who reads a project description closely and writes down only what it says.\n\nYour task is to list the pieces of work the project below needs, using the description's own words. This step does one job: it lists pieces. It does not make steps.\n\nThe project:\n{{ $(\"Inputs\").first().json[\"project\"] }}\n\nRules:\n- Write one row for each thing the description says must be made, booked, sent, told or ready.\n- In \"Piece\", name it in a few words as a thing, such as \"Room with a projector\", not as an action.\n- In \"Quote\", copy the words about it from the description, word for word, as short as you can. Do not reword the quote.\n- If the project needs something the description never mentions, you may add up to two rows for it. Write exactly None found in the Quote cell of those rows.\n- Do not split a piece into steps yet. Do not add a date, a number or a name the description does not give.\n\nReturn one markdown table and nothing else:\n| # | Piece | Quote |{{ $(\"Check 1: Every quote is word for word from the description\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every quote is word for word from the description. What was wrong: \" + $(\"Check 1: Every quote is word for word from the description\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "e6fdd408-9fea-4737-a0fb-6ac636586d05",
      "name": "1. List the pieces the project needs",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Write down each thing the description says must exist, be booked, be sent or be ready, with the words it uses. Returns: A table of the pieces the project needs, each with the description's own words.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every quote is word for word from the description\n// Rule: Every quote in the Quote column appears word for word in the project.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"1. List the pieces the project needs\").first().json.text ?? '');\nconst sources = {\n  \"project\": String($(\"Inputs\").first().json[\"project\"] ?? ''),\n};\nconst result = runRule(\"column_quotes_source\", {\"column\":\"Quote\",\"input\":\"project\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "b1a20820-fbb5-4c9b-a079-0ea2b031ceed",
      "name": "Check 1: Every quote is word for word from the description",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Every quote in the Quote column appears word for word in the project.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "19af2915-3d51-45aa-a577-e7a309d03ce0",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2a7aa8f6-0bef-453f-a4db-21203c315ba1",
      "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": "2425d072-8533-4ac5-a518-89f0eb9b2beb",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7708288e-a875-4835-a900-a0682480844f",
      "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 the pieces the project needs\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c6544387-89cc-4770-ae4d-dcc1fccb2eea",
              "name": "flag",
              "value": "=Every quote is word for word from the description: did not pass. {{ $(\"Check 1: Every quote is word for word from the description\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8454340b-d9d3-4724-a987-aa651b4666f6",
      "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 project coordinator who splits work until each part is one thing a person can do in one sitting.\n\nYour task is to turn each piece below into steps that each do one job. This step does one job: it splits work. It does not put the steps in order.\n\nThe pieces:\n{{ $(\"1. List the pieces the project needs\").first().json.text }}\n\nThe project:\n{{ $(\"Inputs\").first().json[\"project\"] }}\n\nRules:\n- Write each step as a short action that starts with a verb, such as \"Test the projector\".\n- Give each step one job. Do not join two jobs with \"and\" or \"then\", and do not write a list inside one step. If a step needs two jobs, write two steps.\n- In \"Step\", give each step a letter (A, B, C and on). Do not use numbers for steps.\n- In \"Piece\", write the piece the step belongs to. Give every piece at least one step.\n- In \"Done when\", write something a person could see or count, such as \"Hana has replied yes\". Do not write a feeling such as \"the room is sorted\".\n- Do not put an order, a date or a time on a step yet. Do not add a step that belongs to no piece.\n\nReturn one markdown table and nothing else:\n| Step | One job | Piece | Done when |{{ $(\"Check 2: Every step has a job and a way to see it is done\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every step has a job and a way to see it is done. What was wrong: \" + $(\"Check 2: Every step has a job and a way to see it is done\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 7: Each step does one job\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each step does one job. What was wrong: \" + $(\"Check 7: Each step does one job\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "20c92de8-b26e-4e8d-a01c-e21a70bc013f",
      "name": "2. Split each piece into steps of one job",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "Turn every piece into steps that each do one thing, and say how to tell each is done. Returns: A lettered table of steps, each with one job and a way to see it is done.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every step has a job and a way to see it is done\n// Rule: Every row of the table has something written in \"One job\" and \"Done when\" (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                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"2. Split each piece into steps of one job\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"table_cells_filled\", [\"One job\",\"Done when\"], work, sources);\nreturn [{ json: result }];"
      },
      "id": "855693fb-1165-4522-abce-3ae998035490",
      "name": "Check 2: Every step has a job and a way to see it is done",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Every row of the table has something written in \"One job\" and \"Done when\" (a blank or a dash does not count).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "18af2782-3e51-473d-a477-e6100ad03e73",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "aa7323b1-0bed-46a8-a4dd-5fb73c381766",
      "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": "2525d205-8433-4932-a618-8b83ea9b2a58",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5f0a415d-8073-4aa6-a8f9-1b23a4880994",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. Split each piece into steps of one job\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c55441f4-8acc-4903-ad4d-db2efdcb307d",
              "name": "flag",
              "value": "=Every step has a job and a way to see it is done: did not pass. {{ $(\"Check 2: Every step has a job and a way to see it is done\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8554359e-dcd3-4bdd-a687-a5ac1a466563",
      "name": "Flag check 2",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1910,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a scheduler who works out what has to happen before what.\n\nYour task is to put the steps below in an order a person can follow from top to bottom, and say what each step needs first. This step does one job: it orders. It does not estimate time.\n\nThe steps:\n{{ $(\"2. Split each piece into steps of one job\").first().json.text }}\n\nThe project:\n{{ $(\"Inputs\").first().json[\"project\"] }}\n\nRules:\n- In \"Needs first\", write the letters of the steps that must be finished before this one can start, such as B or A and C. If there are none, write exactly Nothing.\n- A step needs another only when it cannot start without it. Do not add a need just because one step is listed before another.\n- Use what the description says about who is free and when, and about what must happen before something else.\n- List the steps so that each one comes after every step it needs.\n- Do not change a step's wording. Do not add a step or drop one.\n\nReturn one markdown table and nothing else:\n| Step | One job | Needs first |{{ $(\"Check 3: Every step says what it needs first\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every step says what it needs first. What was wrong: \" + $(\"Check 3: Every step says what it needs first\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 8: No step comes before one it needs\").isExecuted ? \"\\n\\nYour last answer did not pass this check: No step comes before one it needs. What was wrong: \" + $(\"Check 8: No step comes before one it needs\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "2821280d-b543-4764-a305-80af73b9b05e",
      "name": "3. Put the steps in order",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "Say which steps must be finished before each one can start, and list them so each comes after the ones it needs. Returns: The steps in an order a person can follow, each with what it needs first.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every step says what it needs first\n// Rule: Every row of the table has something written in \"Needs first\" (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                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"3. Put the steps in order\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"table_cells_filled\", [\"Needs first\"], work, sources);\nreturn [{ json: result }];"
      },
      "id": "07faf7be-9d1a-4ec1-a02e-bc248699d84f",
      "name": "Check 3: Every step says what it needs first",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        320
      ],
      "notes": "Every row of the table has something written in \"Needs first\" (a blank or a dash does not count).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "17af25ef-3b51-4284-a777-eac90bd04006",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "aa756248-8bea-4e91-a4e0-67ce3c35d8cf",
      "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": "2225cd4c-8333-479f-a718-8d16ed9b2f11",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "730c9f70-9c71-4823-acfb-2da680859251",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2560,
        520
      ],
      "notes": "True = first failure: send back to \"3. Put the steps in order\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c85446ad-8bcc-4a96-ac4d-d99bfacb2bc4",
              "name": "flag",
              "value": "=Every step says what it needs first: did not pass. {{ $(\"Check 3: Every step says what it needs first\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "86543731-dbd3-4a4a-a787-a73f194663d0",
      "name": "Flag check 3",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2690,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are an experienced planner who estimates honestly and says when something is a guess.\n\nYour task is to say how long each step below takes and who does it. This step does one job: it adds time and a person.\n\nThe ordered steps:\n{{ $(\"3. Put the steps in order\").first().json.text }}\n\nThe project:\n{{ $(\"Inputs\").first().json[\"project\"] }}\n\nRules:\n- In \"Time needed\", use exactly one of these: under an hour, half a day, a day, a few days, a week or more. Count from starting the step to finishing it, including any wait for someone else.\n- Do not write a number in the Time needed column. Use the words above.\n- In \"Who (a role)\", write a person or role the description names, or You for the person who wrote it. If the description names nobody for the step, write Not decided. Do not make up a name.\n- Do not change the order or the wording of the steps.\n\nReturn one markdown table and nothing else:\n| Step | One job | Needs first | Time needed | Who (a role) |\nThe Time needed column must say exactly under an hour, half a day, a day, a few days or a week or more.{{ $(\"Check 4: Every time is one of the five allowed phrases\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every time is one of the five allowed phrases. What was wrong: \" + $(\"Check 4: Every time is one of the five allowed phrases\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "c9d52def-cf7b-40fa-a3e8-5649889085bc",
      "name": "4. Say how long each step takes and who does it",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2820,
        320
      ],
      "notes": "Give each step a time in plain words and a person or role, using only the people the description names. Returns: The ordered steps with a time in plain words and a person or role on each.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every time is one of the five allowed phrases\n// Rule: Every row of the table has one of \"under an hour\", \"half a day\", \"a day\", \"a few days\" or \"a week or more\" in the Time needed column.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"4. Say how long each step takes and who does it\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Time needed\",\"allowed\":[\"under an hour\",\"half a day\",\"a day\",\"a few days\",\"a week or more\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "7856450c-ddfa-412b-a181-53ca41337381",
      "name": "Check 4: Every time is one of the five allowed phrases",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "Every row of the table has one of \"under an hour\", \"half a day\", \"a day\", \"a few days\" or \"a week or more\" in the Time needed column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "16af245c-3c51-4417-a677-e9360cd04199",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2a6ddd03-8be8-4ffa-a4cf-1eadbc29d65c",
      "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": "2325cedf-8233-460c-a818-8ea9ec9b2d7e",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "db0f81bf-a482-4ddc-ad07-f99900798fde",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        520
      ],
      "notes": "True = first failure: send back to \"4. Say how long each step takes and who does it\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c754451a-8ccc-4c29-ab4d-d808fbcb2d57",
              "name": "flag",
              "value": "=Every time is one of the five allowed phrases: did not pass. {{ $(\"Check 4: Every time is one of the five allowed phrases\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "875438c4-d6d3-426b-a487-a28620466ed5",
      "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 project coordinator who writes a plan one person can follow alone.\n\nYour task is to write the plan from the table below and say whether it fits the date. This step does one job: it writes up what the earlier steps decided.\n\nThe table:\n{{ $(\"4. Say how long each step takes and who does it\").first().json.text }}\n\nThe project:\n{{ $(\"Inputs\").first().json[\"project\"] }}\n\nRules:\n- Start with one line that begins \"Goal:\" and says what the project makes and by when, in the description's words.\n- Under \"Steps in order\", copy the table with the columns Step, What to do, Needs first, Time needed and Who. Keep each step's letter, order and wording.\n- Under \"Does it fit?\", write two or three sentences. Add up the times in words, such as \"about two working days\", compare that with the hours and the date the description gives, and say whether it fits. If the description gives no hours or no date, say you cannot tell and name what is missing.\n- Under \"Not in the description\", write up to three questions about anything the plan needs that the description leaves out, each ending with a question mark. If there are none, write \"- None found.\"\n- Write a number only as the description writes it. Write any other amount in words, such as \"four weeks\". Do not add a date, a cost or a number of hours that the description does not give.\n- End with exactly this line: Check the order and the times with the people who will do the work before you start.\n\nReturn markdown in this form and nothing else:\nGoal: (one sentence)\n## Steps in order\n| Step | What to do | Needs first | Time needed | Who |\n## Does it fit?\n(two or three sentences)\n## Not in the description\n(up to three bullets)\nCheck the order and the times with the people who will do the work before you start.{{ $(\"Check 5: The plan ends with the reminder to check it with the people doing the work\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The plan ends with the reminder to check it with the people doing the work. What was wrong: \" + $(\"Check 5: The plan ends with the reminder to check it with the people doing the work\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: Every number in the plan is in the description\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every number in the plan is in the description. What was wrong: \" + $(\"Check 6: Every number in the plan is in the description\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 9: The fit check says only what the description supports\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The fit check says only what the description supports. What was wrong: \" + $(\"Check 9: The fit check says only what the description supports\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "16458dde-3287-4ef7-abad-b134191850cd",
      "name": "5. Write the plan and check it against the date",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3600,
        320
      ],
      "notes": "Put the steps, the times and a plain answer to \"does it fit?\" on one page a person can follow. Returns: A one-page plan with the steps in order, times, people, a fit check and the open questions.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The plan ends with the reminder to check it with the people doing the work\n// Rule: The answer includes the words \"Check the order and the times with the people who will do the work before you start.\".\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"5. Write the plan and check it against the date\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"has_text\", \"Check the order and the times with the people who will do the work before you start.\", work, sources);\nreturn [{ json: result }];"
      },
      "id": "88cf5657-3c93-4f0e-a6fa-e511bfb18890",
      "name": "Check 5: The plan ends with the reminder to check it with the people doing the work",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3860,
        320
      ],
      "notes": "The answer includes the words \"Check the order and the times with the people who will do the work before you start.\".",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "15af22c9-3951-4f5e-a977-edef0dd0432c",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "aa70e51a-8be6-4163-a4d1-5d443c26ce45",
      "name": "Passed check 5?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4120,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2025ca26-8133-4479-a918-903cef9b3237",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ff11f902-8080-4699-a10a-70dc9c76b3db",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4120,
        520
      ],
      "notes": "True = first failure: send back to \"5. Write the plan and check it against the date\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c2543d3b-8dcc-4dbc-a24d-e30df8cb289e",
              "name": "flag",
              "value": "=The plan ends with the reminder to check it with the people doing the work: did not pass. {{ $(\"Check 5: The plan ends with the reminder to check it with the people doing the work\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "88543a57-d5d3-40d8-a587-a4191f466d42",
      "name": "Flag check 5",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4250,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every number in the plan is in the description\n// Rule: Every number in the answer also appears in the project.\n// Runs with no AI. Returns pass (true or false) and a reason.\nconst runRule = function runRule(rule, value, work, sources) {\n    const norm = (s) => String(s ?? '')\n        .toLowerCase()\n        .replace(/[\\u2018\\u2019\\u201c\\u201d\"'`]/g, '')\n        .replace(/\\s+/g, ' ')\n        .trim();\n    const readTable = (text) => {\n        const lines = String(text ?? '')\n            .split(/\\r?\\n/)\n            .map((l) => l.trim());\n        const tables = [];\n        let cur = null;\n        const cells = (l) => l\n            .replace(/^\\|/, '')\n            .replace(/\\|$/, '')\n            .split('|')\n            .map((c) => c.trim());\n        for (let i = 0; i < lines.length; i++) {\n            const l = lines[i] ?? '';\n            if (!l.startsWith('|')) {\n                cur = null;\n                continue;\n            }\n            if (!cur) {\n                const next = lines[i + 1] ?? '';\n                if (/^\\|?\\s*:?-{2,}/.test(next)) {\n                    cur = { header: cells(l), rows: [] };\n                    tables.push(cur);\n                    i++;\n                }\n                continue;\n            }\n            cur.rows.push(cells(l));\n        }\n        return tables;\n    };\n    // Every table that has this column, with \"None found\" rows left out.\n    // Returns that column's cells from every such table, or null if no table has it.\n    const findColumn = (name) => {\n        let seen = false;\n        const cells = [];\n        for (const t of readTable(work)) {\n            const i = t.header.findIndex((h) => norm(h) === norm(name));\n            if (i < 0)\n                continue;\n            seen = true;\n            for (const r of t.rows) {\n                if (r.some((c) => norm(c) === 'none found'))\n                    continue;\n                cells.push(r[i] ?? '');\n            }\n        }\n        return seen ? cells : null;\n    };\n    const empty = (c) => !c || /^[-\\u2013\\u2014\\s]*$/.test(c);\n    switch (rule) {\n        case 'has_text':\n            return norm(work).includes(norm(value))\n                ? { pass: true, reason: 'Found.' }\n                : { pass: false, reason: `The answer does not include \"${value}\".` };\n        case 'lacks_text':\n            return norm(work).includes(norm(value))\n                ? { pass: false, reason: `The answer includes \"${value}\", which it should not.` }\n                : { pass: true, reason: 'Not found, as it should be.' };\n        case 'max_words': {\n            const n = String(work ?? '')\n                .split(/\\s+/)\n                .filter(Boolean).length;\n            return n <= Number(value)\n                ? { pass: true, reason: `${n} words.` }\n                : { pass: false, reason: `The answer has ${n} words; the limit is ${value}.` };\n        }\n        case 'table_cells_filled': {\n            const problems = [];\n            for (const col of value) {\n                const f = findColumn(col);\n                if (!f) {\n                    problems.push(`No table with a \"${col}\" column was found.`);\n                    continue;\n                }\n                f.forEach((c, i) => {\n                    if (empty(c))\n                        problems.push(`Row ${i + 1} has nothing in \"${col}\".`);\n                });\n            }\n            return problems.length\n                ? { pass: false, reason: problems.join(' ') }\n                : { pass: true, reason: 'Every row is filled in.' };\n        }\n        case 'column_values_in': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const allowed = value.allowed.map(norm);\n            const bad = [];\n            f.forEach((c, i) => {\n                if (!allowed.includes(norm(c)))\n                    bad.push(`Row ${i + 1} says \"${c}\".`);\n            });\n            return bad.length\n                ? {\n                    pass: false,\n                    reason: `The \"${value.column}\" column must say ${value.allowed.join(' or ')}. ${bad.join(' ')}`,\n                }\n                : { pass: true, reason: 'Every row uses an allowed word.' };\n        }\n        case 'column_quotes_source': {\n            const f = findColumn(value.column);\n            if (!f)\n                return { pass: false, reason: `No table with a \"${value.column}\" column was found.` };\n            const src = norm(sources[value.input] ?? '');\n            const bad = [];\n            f.forEach((c, i) => {\n                // A quote may be shortened with \"...\"; each piece must be in the source.\n                const pieces = c\n                    .split(/\\.\\.\\.|\\u2026/)\n                    .map(norm)\n                    .filter(Boolean);\n                if (!pieces.length || !pieces.every((p) => src.includes(p)))\n                    bad.push(`Row ${i + 1}: \"${c}\" is not in the source.`);\n            });\n            return bad.length\n                ? { pass: false, reason: bad.join(' ') }\n                : { pass: true, reason: 'Every quote was found in the source.' };\n        }\n        case 'numbers_in_source': {\n            const nums = (s) => (String(s ?? '').match(/\\d[\\d,]*(?:\\.\\d+)?/g) ?? []).map((n) => n.replace(/,/g, ''));\n            const src = new Set(nums(sources[value.input] ?? ''));\n            const missing = [...new Set(nums(work))].filter((n) => !src.has(n));\n            return missing.length\n                ? {\n                    pass: false,\n                    reason: `These numbers are not in the source: ${missing.join(', ')}.`,\n                }\n                : { pass: true, reason: 'Every number was found in the source.' };\n        }\n        default:\n            return { pass: false, reason: `Unknown rule \"${rule}\".` };\n    }\n};\n\nconst work = String($(\"5. Write the plan and check it against the date\").first().json.text ?? '');\nconst sources = {\n  \"project\": String($(\"Inputs\").first().json[\"project\"] ?? ''),\n};\nconst result = runRule(\"numbers_in_source\", {\"input\":\"project\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "af3ff13b-f938-492a-a08d-4419e57b4940",
      "name": "Check 6: Every number in the plan is in the description",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4380,
        320
      ],
      "notes": "Every number in the answer also appears in the project.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "14af2136-3a51-40f1-a877-ec5c0ed044bf",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2a695fd5-0be3-494c-a4d3-9bdbbc2e538a",
      "name": "Passed check 6?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4640,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2125cbb9-8033-42e6-aa18-91cfee9b30a4",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f7142b01-287d-4d7a-a102-b937fc7e06c0",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4640,
        520
      ],
      "notes": "True = first failure: send back to \"5. Write the plan and check it against the date\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1543ba8-8ecc-4f4f-a14d-e17af9cb2a31",
              "name": "flag",
              "value": "=Every number in the plan is in the description: did not pass. {{ $(\"Check 6: Every number in the plan is in the description\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "89543bea-d8d3-4591-a287-9f601e466baf",
      "name": "Flag check 6",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4770,
        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 step does one job\nRead the \"One job\" column in the answer. Does any step name two jobs, joined by \"and\", \"then\", a comma or a list? Answer FAIL and quote each step that does.\n\nThe work to check (the answer from Step 2 (Split each piece into steps of one job)):\n{{ $(\"2. Split each piece into steps of one job\").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": "3b3e12d0-c385-40ad-a7f8-bdd2c0a3f427",
      "name": "Check 7: Each step does one job",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4900,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the \"One job\" column in the answer. Does any step name two jobs, joined by \"and\", \"then\", a comma or a list? Answer FAIL and quote each step that does.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "13af1fa3-3751-4c38-ab77-f1150fd04652",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2a6b9e6c-0be0-4ab5-a4d5-da72bc2c14f3",
      "name": "Passed check 7?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5160,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "1e25c700-7f33-4153-ab18-9362f19b355d",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5b170704-047a-4637-a505-307a287c0d6d",
      "name": "Try again? (check 7)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5160,
        520
      ],
      "notes": "True = first failure: send back to \"2. Split each piece into steps of one job\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c4544061-8fcc-40e2-a04d-dfe7f6cb2578",
              "name": "flag",
              "value": "=Each step does one job: did not pass. {{ $(\"Check 7: Each step does one job\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8a543d7d-d7d3-43fe-a387-a0f31d466a1c",
      "name": "Flag check 7",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5290,
        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 step comes before one it needs\nRead the table from top to bottom. Does any step come before a step listed in its \"Needs first\" cell, or name a letter that is not a step in the table? Answer FAIL and name each step that does.\n\nThe work to check (the answer from Step 3 (Put the steps in order)):\n{{ $(\"3. Put the steps in order\").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": "be01110b-8d73-4ebc-a64f-6e91117891ba",
      "name": "Check 8: No step comes before one it needs",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        5420,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the table from top to bottom. Does any step come before a step listed in its \"Needs first\" cell, or name a letter that is not a step in the table? Answer FAIL and name each step that does.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "12af1e10-3851-4dcb-aa77-ef8210d047e5",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "aa641927-0bde-4c1e-a4ec-6a41bc1f4900",
      "name": "Passed check 8?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5680,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "1f25c893-7e33-4fc0-ac18-94f5f09b33ca",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "73196b63-9c64-4c30-a4e9-f11d8896e80a",
      "name": "Try again? (check 8)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5680,
        520
      ],
      "notes": "True = first failure: send back to \"3. Put the steps in order\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c3543ece-90cc-4275-af4d-de54f7cb270b",
              "name": "flag",
              "value": "=No step comes before one it needs: did not pass. {{ $(\"Check 8: No step comes before one it needs\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7b5425e0-d2d3-4c1f-a087-b56a24467521",
      "name": "Flag check 8",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        5810,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: The fit check says only what the description supports\nRead the \"Does it fit?\" section. Does it say a plan fits or does not fit using only the hours and the date in the project description? If the description gives no hours or no date, does it say so instead of guessing? Answer FAIL and quote each claim the description does not support.\n\nThe project:\n{{ $(\"Inputs\").first().json[\"project\"] }}\n\nThe work to check (the answer from Step 5 (Write the plan and check it against the date)):\n{{ $(\"5. Write the plan and check it against the date\").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": "fbfd6e9a-995d-4989-af45-e0c8769cfb07",
      "name": "Check 9: The fit check says only what the description supports",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        5940,
        320
      ],
      "notes": "Answers PASS or FAIL. Read the \"Does it fit?\" section. Does it say a plan fits or does not fit using only the hours and the date in the project description? If the description gives no hours or no date, does it say so instead of guessing? Answer FAIL and quote each claim the description does not support.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "21af35ad-3551-4912-ad77-f43b11d04978",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "aa6657be-0bdc-4d87-a4ee-a8d8bc1d0a69",
      "name": "Passed check 9?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6200,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "2c25dd0a-8d33-475d-ad18-9688e39b1f53",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "571b7de6-8861-4e1d-a8ec-4f30a494d587",
      "name": "Try again? (check 9)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        6200,
        520
      ],
      "notes": "True = first failure: send back to \"5. Write the plan and check it against the date\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "be5436ef-91cc-4408-a64d-d029f4cb2252",
              "name": "flag",
              "value": "=The fit check says only what the description supports: did not pass. {{ $(\"Check 9: The fit check says only what the description supports\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7c542773-d1d3-4a8c-a187-b6fd2346738e",
      "name": "Flag check 9",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        6330,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 10: check by hand**\nRead the plan with the person who will do the work. Ask whether any step must come earlier or later than shown and whether each time matches how long it takes them. Only they know how long their own work takes.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "8d3d38e2-52b7-4c69-a36b-1b6ce1c4ddbb",
      "name": "Check 10 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        6180,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "817b1682-f907-4f4b-ae9d-4968125ee671",
              "name": "result",
              "value": "={{ $(\"5. Write the plan and check it against the date\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "ff52b9db-d4dd-4360-a6c4-da19636e062e",
              "name": "flagged",
              "value": "={{ [$(\"Flag check 1\").isExecuted ? $(\"Flag check 1\").first().json.flag : \"\", $(\"Flag check 2\").isExecuted ? $(\"Flag check 2\").first().json.flag : \"\", $(\"Flag check 3\").isExecuted ? $(\"Flag check 3\").first().json.flag : \"\", $(\"Flag check 4\").isExecuted ? $(\"Flag check 4\").first().json.flag : \"\", $(\"Flag check 5\").isExecuted ? $(\"Flag check 5\").first().json.flag : \"\", $(\"Flag check 6\").isExecuted ? $(\"Flag check 6\").first().json.flag : \"\", $(\"Flag check 7\").isExecuted ? $(\"Flag check 7\").first().json.flag : \"\", $(\"Flag check 8\").isExecuted ? $(\"Flag check 8\").first().json.flag : \"\", $(\"Flag check 9\").isExecuted ? $(\"Flag check 9\").first().json.flag : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "ebc95c31-b4fb-45be-a9c0-d7df683fb9fc",
              "name": "check_by_hand",
              "value": "- Read the plan with the person who will do the work. Ask whether any step must come earlier or later than shown and whether each time matches how long it takes them. Only they know how long their own work takes.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "e7619f77-da0d-41dc-ace0-6c650a4c00a2",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        6460,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "50ef102d-94db-442e-ac1c-a92bedeae24c",
      "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 the pieces the project needs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. List the pieces the project needs": {
      "main": [
        [
          {
            "node": "Check 1: Every quote is word for word from the description",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Every quote is word for word from the description": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. Split each piece into steps of one job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. List the pieces the project needs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. Split each piece into steps of one job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Split each piece into steps of one job": {
      "main": [
        [
          {
            "node": "Check 2: Every step has a job and a way to see it is done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Every step has a job and a way to see it is done": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Put the steps in order",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. Split each piece into steps of one job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Put the steps in order",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Put the steps in order": {
      "main": [
        [
          {
            "node": "Check 3: Every step says what it needs first",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: Every step says what it needs first": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "4. Say how long each step takes and who does it",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "3. Put the steps in order",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "4. Say how long each step takes and who does it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Say how long each step takes and who does it": {
      "main": [
        [
          {
            "node": "Check 4: Every time is one of the five allowed phrases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: Every time is one of the five allowed phrases": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "5. Write the plan and check it against the date",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Say how long each step takes and who does it",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "5. Write the plan and check it against the date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Write the plan and check it against the date": {
      "main": [
        [
          {
            "node": "Check 5: The plan ends with the reminder to check it with the people doing the work",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: The plan ends with the reminder to check it with the people doing the work": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: Every number in the plan is in the description",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "5. Write the plan and check it against the date",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: Every number in the plan is in the description",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: Every number in the plan is in the description": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Check 7: Each step does one job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "5. Write the plan and check it against the date",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Check 7: Each step does one job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 7: Each step does one job": {
      "main": [
        [
          {
            "node": "Passed check 7?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 7?": {
      "main": [
        [
          {
            "node": "Check 8: No step comes before one it needs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 7)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 7)": {
      "main": [
        [
          {
            "node": "2. Split each piece into steps of one job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 7": {
      "main": [
        [
          {
            "node": "Check 8: No step comes before one it needs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 8: No step comes before one it needs": {
      "main": [
        [
          {
            "node": "Passed check 8?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 8?": {
      "main": [
        [
          {
            "node": "Check 9: The fit check says only what the description supports",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 8)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 8)": {
      "main": [
        [
          {
            "node": "3. Put the steps in order",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 8": {
      "main": [
        [
          {
            "node": "Check 9: The fit check says only what the description supports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 9: The fit check says only what the description supports": {
      "main": [
        [
          {
            "node": "Passed check 9?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 9?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 9)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 9)": {
      "main": [
        [
          {
            "node": "5. Write the plan and check it against the date",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 9": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. List the pieces the project needs",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. Split each piece into steps of one job",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Put the steps in order",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Say how long each step takes and who does it",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "5. Write the plan and check it against the date",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 7: Each step does one job",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 8: No step comes before one it needs",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 9: The fit check says only what the description supports",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "break-a-project-into-steps"
  }
}
