{
  "name": "Write a scope of work",
  "nodes": [
    {
      "parameters": {
        "content": "## Write a scope of work\nTurn rough notes about a piece of work into a scope of work that says what will be delivered, how each part is accepted, what is left out and what is still unknown, without inventing any detail.\n\n**How to use**\n1. Open **Chat model** and pick your own credential. To use another AI service, delete that node, add the service's chat model node, and connect it to the Model plug under every step and AI check.\n2. Open **Inputs** and paste your material into each field. Examples are filled in so you can try it first.\n3. Click **Execute workflow**.\n4. Open **Result** to read the answer, anything flagged, and the checks left for you.\n\n**Checks**\n- Check 1 (a plain rule, run automatically with no AI): Every row of the table has one of \"stated\", \"missing\" or \"unclear\" in the Status column. 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 \"Deliverable\", \"Accepted when\" and \"Due\" (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): The answer is no longer than 600 words. If it fails: try once more, then flag.\n- Check 4 (a plain rule, run automatically with no AI): Every number in the answer also appears in the your notes about the work. If it fails: try once more, then flag.\n- Check 5 (a second AI prompt answers pass or fail): Compare the draft with the notes below. Is every name, date, amount and promise in the draft found in the notes? Answer FAIL and quote anything that is not, including a choice the draft made between two figures that the notes disagree on. If it fails: try once more, then flag.\n- Check 6 (a second AI prompt answers pass or fail): Compare the section \"Questions to settle first\" with the table of what is known below. Is every item marked missing or unclear in the table asked as a question, and is none of them written as settled anywhere else in the draft? Answer FAIL and name the item. If it fails: try once more, then flag.\n- Check 7 (you check it by hand): Read the draft as the person who will do the work, or the person who will approve it. Could they start without asking anything that is not already in \"Questions to settle first\"? Mark anything you would not want to commit to, and fix those lines in your own words before you send it. If it fails: flag it and carry on.\n\nFrom Logic Lab (logiclabhq.com). Free to use and adapt, with credit.",
        "height": 780,
        "width": 760,
        "color": 5
      },
      "id": "cb9fd71c-b3af-4b07-a3dd-1c72563f06e5",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -740
      ]
    },
    {
      "parameters": {},
      "id": "1d63bfc8-54da-4ab7-ac7a-a8860dc0cd0d",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "1dc830ca-0fd8-4409-a27b-7824dd44012b",
              "name": "notes",
              "value": "Sample: We run a small community arts center and need someone to photograph our spring festival on Saturday, April 11. About 300 people usually come. We want edited photos for the website and a printed flyer, by the end of April. Budget is around 900 dollars. Our events coordinator will be on site and can sign off.",
              "type": "string"
            },
            {
              "id": "5b647d2f-a012-4b8c-a876-3c5df7034bda",
              "name": "parties",
              "value": "Sample: Our arts center is asking; a freelance photographer will do the work",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6a513b61-74b3-4300-abcb-0fab54538002",
      "name": "Inputs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        320
      ],
      "notes": "Paste your own material here: notes = Your notes about the work; parties = Who is asking and who will do the work.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are an experienced project manager who writes scopes of work for people who are not specialists.\n\nYour task is to sort the notes below into what is known and what is missing, for each part of a scope of work.\n\nThe notes about the work:\n{{ $(\"Inputs\").first().json[\"notes\"] }}\n\nWho is asking and who will do the work: {{ $(\"Inputs\").first().json[\"parties\"] }}\n\nRules:\n- Use exactly these eight items, in this order: Purpose, What is delivered, Dates, Budget, Who approves, What the asker provides, Who does what, Limits on the work.\n- In \"What the notes say\", use the notes' own words, shortened. If the notes say two different things about an item, write both.\n- Mark Status \"stated\" if the notes give the item, \"missing\" if they do not, and \"unclear\" if the notes disagree with themselves or only hint at it.\n- For a missing item write \"Not in the notes\". Do not guess and do not fill gaps. Do not add items.\n\nReturn one markdown table and nothing else:\n| # | Item | What the notes say | Status |\nThe Status column must say exactly stated, missing or unclear.{{ $(\"Check 1: Each item is marked stated, missing or unclear\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Each item is marked stated, missing or unclear. What was wrong: \" + $(\"Check 1: Each item is marked stated, missing or unclear\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "06fdce2a-c1e2-4917-af84-390c5a8321e1",
      "name": "1. Sort what is known from what is missing",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        480,
        320
      ],
      "notes": "Go through the parts of a scope of work and mark each as stated in the notes, missing or unclear. Returns: A table of eight items, each marked stated, missing or unclear.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Each item is marked stated, missing or unclear\n// Rule: Every row of the table has one of \"stated\", \"missing\" or \"unclear\" in the Status 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($(\"1. Sort what is known from what is missing\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"column_values_in\", {\"column\":\"Status\",\"allowed\":[\"stated\",\"missing\",\"unclear\"]}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "bed86b8f-041a-4d60-a287-bd8544e8e7de",
      "name": "Check 1: Each item is marked stated, missing or unclear",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        320
      ],
      "notes": "Every row of the table has one of \"stated\", \"missing\" or \"unclear\" in the Status column.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "ffcc50fb-a2b5-48ea-aaea-34cd35deb49c",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0a36fca8-019f-437f-a5c4-eb1e4569bd25",
      "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": "be4cc6a8-acab-4005-af75-678ada61c447",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a4c691f0-7c06-4ef5-abc3-cc567a75a8f3",
      "name": "Try again? (check 1)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1000,
        520
      ],
      "notes": "True = first failure: send back to \"1. Sort what is known from what is missing\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c57a5369-2e9c-42b0-a571-3bcf4f175bb6",
              "name": "flag",
              "value": "=Each item is marked stated, missing or unclear: did not pass. {{ $(\"Check 1: Each item is marked stated, missing or unclear\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7e57d57d-0fa4-41e4-a319-0c2328fadc32",
      "name": "Flag check 1",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1130,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful writer of agreements who makes finished work easy to recognize.\n\nYour task is to list every deliverable the notes ask for, with how it will be accepted and when it is due.\n\nThe notes about the work:\n{{ $(\"Inputs\").first().json[\"notes\"] }}\n\nWhat is known and missing:\n{{ $(\"1. Sort what is known from what is missing\").first().json.text }}\n\nRules:\n- A deliverable is something that is handed over, such as a file, a report or an event. Do not list activities.\n- \"Accepted when\" must be something the person approving can see, count or open. \"It looks professional\" is not acceptable. \"The edited files are in the shared folder and each one opens\" is.\n- Use a number in \"Accepted when\" only if the notes give it. Otherwise say what the approver would look at.\n- \"Due\" must be a date or phrase from the notes. If the notes give none, write \"to confirm\". Do not make up a date.\n- Do not add deliverables the notes do not ask for.\n\nReturn one markdown table and nothing else:\n| # | Deliverable | Accepted when | Due |{{ $(\"Check 2: Every deliverable has a way to accept it and a due date\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every deliverable has a way to accept it and a due date. What was wrong: \" + $(\"Check 2: Every deliverable has a way to accept it and a due date\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "0e90f4c5-14a2-440a-a36a-4f4f04fcb0e4",
      "name": "2. List each deliverable and how it is accepted",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1260,
        320
      ],
      "notes": "Name each thing that will be handed over, what the approver will see when it is done, and when it is due. Returns: A table of each deliverable with how it is accepted and when it is due.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every deliverable has a way to accept it and a due date\n// Rule: Every row of the table has something written in \"Deliverable\", \"Accepted when\" and \"Due\" (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. List each deliverable and how it is accepted\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"table_cells_filled\", [\"Deliverable\",\"Accepted when\",\"Due\"], work, sources);\nreturn [{ json: result }];"
      },
      "id": "7b55d772-fee9-445b-aaeb-ac846938ec75",
      "name": "Check 2: Every deliverable has a way to accept it and a due date",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        320
      ],
      "notes": "Every row of the table has something written in \"Deliverable\", \"Accepted when\" and \"Due\" (a blank or a dash does not count).",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "fecc4f68-a3b5-4a7d-a9ea-333a36deb62f",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0a393b3f-019d-44e8-a5bd-65d9c571426a",
      "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": "bf4cc83b-abaa-4e72-a075-691dd961c2b4",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "84bfa3cb-5404-4166-a3c6-aea5fa7d2e38",
      "name": "Try again? (check 2)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        520
      ],
      "notes": "True = first failure: send back to \"2. List each deliverable and how it is accepted\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c47a51d6-2f9c-4443-a471-3a3c50175d49",
              "name": "flag",
              "value": "=Every deliverable has a way to accept it and a due date: did not pass. {{ $(\"Check 2: Every deliverable has a way to accept it and a due date\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7b57d0c4-12a4-469d-a419-0db627fada9f",
      "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 practical adviser who stops work from growing after it has started.\n\nYour task is to set the edges of the work: what is not included, what is being assumed, and what the asker must provide.\n\nThe notes about the work:\n{{ $(\"Inputs\").first().json[\"notes\"] }}\n\nWhat is known and missing:\n{{ $(\"1. Sort what is known from what is missing\").first().json.text }}\n\nThe deliverables:\n{{ $(\"2. List each deliverable and how it is accepted\").first().json.text }}\n\nRules:\n- Under \"Not included\", list work someone might expect that the notes do not ask for. Write each as a short line.\n- Under \"Assumptions\", list only what the notes imply and the work depends on, such as access or a decision by a certain date. Do not turn a missing item into an assumption. Leave it out so it can be asked.\n- Under \"What the asker provides\", list only what the notes say the asker will give or do.\n- Do not invent a price, a date, a person or a promise. Do not write more than five lines under any heading.\n\nReturn a short list under three headings: Not included, Assumptions, What the asker provides."
      },
      "id": "40abc02f-1f7b-41da-ae18-93e5307f2970",
      "name": "3. Set the edges of the work",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2040,
        320
      ],
      "notes": "List what is not included, what is assumed and what the asker provides, using only what the notes support. Returns: Three short lists that set the edges of the work.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a clear writer who drafts scopes of work that both sides can read in five minutes.\n\nYour task is to write the scope of work from the material below.\n\nThe notes about the work:\n{{ $(\"Inputs\").first().json[\"notes\"] }}\n\nWho is asking and who will do the work: {{ $(\"Inputs\").first().json[\"parties\"] }}\n\nWhat is known and missing:\n{{ $(\"1. Sort what is known from what is missing\").first().json.text }}\n\nThe deliverables:\n{{ $(\"2. List each deliverable and how it is accepted\").first().json.text }}\n\nThe edges of the work:\n{{ $(\"3. Set the edges of the work\").first().json.text }}\n\nRules:\n- Use plain words and keep it to 600 words or fewer. Call the two sides by the roles in the line above about who is asking.\n- Use only facts from the material above. Write every date, amount and name as the notes write it.\n- Do not number the headings or the lines, and do not add any figure the notes do not contain.\n- Anything marked \"missing\" or \"unclear\" goes under \"Questions to settle first\", worded as a question. Do not decide it yourself, and do not choose between two figures that disagree.\n- Do not add payment terms, ownership of the work or legal wording unless the notes give them.\n\nReturn markdown with these headings in this order and nothing else:\n## Purpose\n## What will be delivered\n## Timeline\n## Not included\n## Assumptions\n## What the asker provides\n## How the work is accepted\n## Questions to settle first{{ $(\"Check 3: The scope of work is 600 words or fewer\").isExecuted ? \"\\n\\nYour last answer did not pass this check: The scope of work is 600 words or fewer. What was wrong: \" + $(\"Check 3: The scope of work is 600 words or fewer\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 4: Every figure in the draft is in the notes\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every figure in the draft is in the notes. What was wrong: \" + $(\"Check 4: Every figure in the draft is in the notes\").first().json.reason + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 5: Every name, date and amount comes from the notes\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every name, date and amount comes from the notes. What was wrong: \" + $(\"Check 5: Every name, date and amount comes from the notes\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}{{ $(\"Check 6: Every missing or unclear item is asked as a question\").isExecuted ? \"\\n\\nYour last answer did not pass this check: Every missing or unclear item is asked as a question. What was wrong: \" + $(\"Check 6: Every missing or unclear item is asked as a question\").first().json.text + \"\\nFix that and give your full answer again.\" : \"\" }}"
      },
      "id": "1df0cc6c-0269-4b31-a7eb-f4be73b3135b",
      "name": "4. Write the scope of work",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        2300,
        320
      ],
      "notes": "Put the deliverables, edges and open questions into one scope of work that both sides can read in five minutes. Returns: A scope of work with eight headings, ending in the questions still to settle.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// The scope of work is 600 words or fewer\n// Rule: The answer is no longer than 600 words.\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. Write the scope of work\").first().json.text ?? '');\nconst sources = {\n\n};\nconst result = runRule(\"max_words\", 600, work, sources);\nreturn [{ json: result }];"
      },
      "id": "930e0089-d58d-4bac-ac18-dc4f48d116ba",
      "name": "Check 3: The scope of work is 600 words or fewer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2560,
        320
      ],
      "notes": "The answer is no longer than 600 words.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "01cc5421-a0b5-45c4-a8ea-31a737deb7c2",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0a3b79d6-819a-4cd1-a5bf-a470456e3a53",
      "name": "Passed check 3?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2820,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c04cc9ce-aaaa-4cdf-ad75-6464dc61c76d",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a8c21b0e-7002-4ee3-a7c8-da58867a3905",
      "name": "Try again? (check 3)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2820,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the scope of work\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c37a5043-309c-45d6-a771-3ef54d175890",
              "name": "flag",
              "value": "=The scope of work is 600 words or fewer: did not pass. {{ $(\"Check 3: The scope of work is 600 words or fewer\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7c57d257-11a4-450a-a519-0f4926fad90c",
      "name": "Flag check 3",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2950,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Every figure in the draft is in the notes\n// Rule: Every number in the answer also appears in the your notes about the work.\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. Write the scope of work\").first().json.text ?? '');\nconst sources = {\n  \"notes\": String($(\"Inputs\").first().json[\"notes\"] ?? ''),\n};\nconst result = runRule(\"numbers_in_source\", {\"input\":\"notes\"}, work, sources);\nreturn [{ json: result }];"
      },
      "id": "3cfb2ab7-e955-4ee2-ac8d-84652a569f60",
      "name": "Check 4: Every figure in the draft is in the notes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        320
      ],
      "notes": "Every number in the answer also appears in the your notes about the work.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "00cc528e-a1b5-4757-a7ea-301438deb955",
              "leftValue": "={{ $json.pass === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0a2a30b5-8198-4e3a-a5b8-1f2bc575bf98",
      "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": "c14ccb61-a9aa-4b4c-ae75-65f7db61c5da",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "28ce1d81-7813-449c-afcb-57e70681be4a",
      "name": "Try again? (check 4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3340,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the scope of work\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c27a4eb0-319c-4769-a671-3d624e175a23",
              "name": "flag",
              "value": "=Every figure in the draft is in the notes: did not pass. {{ $(\"Check 4: Every figure in the draft is in the notes\").first().json.reason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7957cd9e-0ca4-4d2b-a619-10dc25fad779",
      "name": "Flag check 4",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3470,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every name, date and amount comes from the notes\nCompare the draft with the notes below. Is every name, date, amount and promise in the draft found in the notes? Answer FAIL and quote anything that is not, including a choice the draft made between two figures that the notes disagree on.\n\nThe notes about the work:\n{{ $(\"Inputs\").first().json[\"notes\"] }}\n\nThe work to check (the answer from Step 4 (Write the scope of work)):\n{{ $(\"4. Write the scope of work\").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": "9d560d4b-b7dd-40c8-ab3b-115df656170a",
      "name": "Check 5: Every name, date and amount comes from the notes",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3600,
        320
      ],
      "notes": "Answers PASS or FAIL. Compare the draft with the notes below. Is every name, date, amount and promise in the draft found in the notes? Answer FAIL and quote anything that is not, including a choice the draft made between two figures that the notes disagree on.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "03cc5747-9eb5-429e-a6ea-2e8131deae50",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0a2c6f4c-8195-4fa3-a5bb-2742c5738101",
      "name": "Passed check 5?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c24cccf4-a8aa-49b9-ab75-613ed661bdfb",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "8cd0f984-5411-4d59-a3ce-33ea227fabc7",
      "name": "Try again? (check 5)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3860,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the scope of work\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c97a59b5-329c-48fc-a171-358353176202",
              "name": "flag",
              "value": "=Every name, date and amount comes from the notes: did not pass. {{ $(\"Check 5: Every name, date and amount comes from the notes\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7a57cf31-0ba4-4b98-a719-126f24fad5e6",
      "name": "Flag check 5",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3990,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "promptType": "define",
        "hasOutputParser": false,
        "text": "=You are a careful reviewer. You do not fix or rewrite the work; you only decide whether it passes one check.\n\nThe check: Every missing or unclear item is asked as a question\nCompare the section \"Questions to settle first\" with the table of what is known below. Is every item marked missing or unclear in the table asked as a question, and is none of them written as settled anywhere else in the draft? Answer FAIL and name the item.\n\nWhat is known and missing:\n{{ $(\"1. Sort what is known from what is missing\").first().json.text }}\n\nThe work to check (the answer from Step 4 (Write the scope of work)):\n{{ $(\"4. Write the scope of work\").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": "1ac4b010-76a6-4713-a7cf-7eea97dc2f15",
      "name": "Check 6: Every missing or unclear item is asked as a question",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        4120,
        320
      ],
      "notes": "Answers PASS or FAIL. Compare the section \"Questions to settle first\" with the table of what is known below. Is every item marked missing or unclear in the table asked as a question, and is none of them written as settled anywhere else in the draft? Answer FAIL and name the item.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "02cc55b4-9fb5-4431-a5ea-2cee32deafe3",
              "leftValue": "={{ String($json.text ?? '').trim().toUpperCase().startsWith('PASS') }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "8a2f7763-0192-478c-a5b3-a1fd457b0646",
      "name": "Passed check 6?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        320
      ],
      "notes": "True = passed. False = did not pass.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c34cce87-a7aa-4826-ac75-62d1d561bc68",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "0cc9743f-fc0e-443a-abd0-98498286feac",
      "name": "Try again? (check 6)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4380,
        520
      ],
      "notes": "True = first failure: send back to \"4. Write the scope of work\" once. False = failed twice: flag it.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c87a5822-339c-4a8f-a071-33f054176395",
              "name": "flag",
              "value": "=Every missing or unclear item is asked as a question: did not pass. {{ $(\"Check 6: Every missing or unclear item is asked as a question\").first().json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7757ca78-0ea4-4051-a819-140223fad453",
      "name": "Flag check 6",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4510,
        700
      ],
      "notes": "Notes the problem and carries on. The note shows in Result.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "content": "**Check 7: check by hand**\nRead the draft as the person who will do the work, or the person who will approve it. Could they start without asking anything that is not already in \"Questions to settle first\"? Mark anything you would not want to commit to, and fix those lines in your own words before you send it.\n\nIf not: flag it and carry on. This check is also listed in Result.",
        "height": 200,
        "width": 240,
        "color": 3
      },
      "id": "0ae35aa0-36fd-42a3-a05a-950ea1d408c1",
      "name": "Check 7 (by hand)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        4360,
        70
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ae22e950-27a2-440b-ad2f-3daafe7287a5",
              "name": "result",
              "value": "={{ $(\"4. Write the scope of work\").first().json.text }}",
              "type": "string"
            },
            {
              "id": "f8cdfff1-2bda-4720-a152-92d31fa07a3a",
              "name": "flagged",
              "value": "={{ [$(\"Flag check 1\").isExecuted ? $(\"Flag check 1\").first().json.flag : \"\", $(\"Flag check 2\").isExecuted ? $(\"Flag check 2\").first().json.flag : \"\", $(\"Flag check 3\").isExecuted ? $(\"Flag check 3\").first().json.flag : \"\", $(\"Flag check 4\").isExecuted ? $(\"Flag check 4\").first().json.flag : \"\", $(\"Flag check 5\").isExecuted ? $(\"Flag check 5\").first().json.flag : \"\", $(\"Flag check 6\").isExecuted ? $(\"Flag check 6\").first().json.flag : \"\"].filter(x => x).join(\"\\n\") || \"None\" }}",
              "type": "string"
            },
            {
              "id": "3a4b0bf7-27c0-477e-ae17-f3893b1a66a8",
              "name": "check_by_hand",
              "value": "- Read the draft as the person who will do the work, or the person who will approve it. Could they start without asking anything that is not already in \"Questions to settle first\"? Mark anything you would not want to commit to, and fix those lines in your own words before you send it.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "266c1ebd-a4e8-451c-a8f7-04cf014f2d1e",
      "name": "Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4640,
        320
      ],
      "notes": "The final answer, anything flagged, and the checks left for you.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "d0c374d3-d77d-4dee-afcd-78b581e5de78",
      "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. Sort what is known from what is missing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Sort what is known from what is missing": {
      "main": [
        [
          {
            "node": "Check 1: Each item is marked stated, missing or unclear",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 1: Each item is marked stated, missing or unclear": {
      "main": [
        [
          {
            "node": "Passed check 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 1?": {
      "main": [
        [
          {
            "node": "2. List each deliverable and how it is accepted",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 1)": {
      "main": [
        [
          {
            "node": "1. Sort what is known from what is missing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 1": {
      "main": [
        [
          {
            "node": "2. List each deliverable and how it is accepted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. List each deliverable and how it is accepted": {
      "main": [
        [
          {
            "node": "Check 2: Every deliverable has a way to accept it and a due date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 2: Every deliverable has a way to accept it and a due date": {
      "main": [
        [
          {
            "node": "Passed check 2?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 2?": {
      "main": [
        [
          {
            "node": "3. Set the edges of the work",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 2)": {
      "main": [
        [
          {
            "node": "2. List each deliverable and how it is accepted",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 2": {
      "main": [
        [
          {
            "node": "3. Set the edges of the work",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Set the edges of the work": {
      "main": [
        [
          {
            "node": "4. Write the scope of work",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Write the scope of work": {
      "main": [
        [
          {
            "node": "Check 3: The scope of work is 600 words or fewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 3: The scope of work is 600 words or fewer": {
      "main": [
        [
          {
            "node": "Passed check 3?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 3?": {
      "main": [
        [
          {
            "node": "Check 4: Every figure in the draft is in the notes",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 3)": {
      "main": [
        [
          {
            "node": "4. Write the scope of work",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 3": {
      "main": [
        [
          {
            "node": "Check 4: Every figure in the draft is in the notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 4: Every figure in the draft is in the notes": {
      "main": [
        [
          {
            "node": "Passed check 4?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 4?": {
      "main": [
        [
          {
            "node": "Check 5: Every name, date and amount comes from the notes",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 4)": {
      "main": [
        [
          {
            "node": "4. Write the scope of work",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 4": {
      "main": [
        [
          {
            "node": "Check 5: Every name, date and amount comes from the notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 5: Every name, date and amount comes from the notes": {
      "main": [
        [
          {
            "node": "Passed check 5?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 5?": {
      "main": [
        [
          {
            "node": "Check 6: Every missing or unclear item is asked as a question",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 5)": {
      "main": [
        [
          {
            "node": "4. Write the scope of work",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 5": {
      "main": [
        [
          {
            "node": "Check 6: Every missing or unclear item is asked as a question",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check 6: Every missing or unclear item is asked as a question": {
      "main": [
        [
          {
            "node": "Passed check 6?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passed check 6?": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Try again? (check 6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Try again? (check 6)": {
      "main": [
        [
          {
            "node": "4. Write the scope of work",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag check 6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag check 6": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat model": {
      "ai_languageModel": [
        [
          {
            "node": "1. Sort what is known from what is missing",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "2. List each deliverable and how it is accepted",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "3. Set the edges of the work",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "4. Write the scope of work",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 5: Every name, date and amount comes from the notes",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Check 6: Every missing or unclear item is asked as a question",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "template": "write-a-scope-of-work"
  }
}
