{
  "name": "Create invoice drafts from Google Sheets on request via Google Chat and Claude",
  "nodes": [
    {
      "id": "c3000000-0000-4000-8000-000000000001",
      "name": "Request form",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.6,
      "position": [
        0,
        0
      ],
      "parameters": {
        "formTitle": "Create an invoice",
        "formDescription": "Write who and which month, e.g. \"Aoba, September\" or \"Blue Harbor の先月分\". A Gmail draft with the PDF is created; nothing is sent.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Request",
              "fieldName": "Request",
              "fieldType": "textarea",
              "requiredField": true,
              "placeholder": "Aoba Trading, September"
            }
          ]
        },
        "responseMode": "lastNode",
        "options": {
          "path": "invoice-request"
        }
      },
      "webhookId": "6c1f8e8a-2a4b-4d7e-9a51-0b3f5c2d7e11"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000002",
      "name": "Google Chat event",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -660,
        400
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "invoice-chat-REPLACE-WITH-A-LONG-RANDOM-STRING",
        "responseMode": "responseNode",
        "options": {}
      },
      "webhookId": "a7d3c9e1-4b2f-4e6a-9c8d-3f1b5e7a2c90"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000003",
      "name": "Chat settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -440,
        400
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": true,
        "assignments": {
          "assignments": [
            {
              "id": "h01",
              "name": "allowed_chat_users",
              "value": "you@example.com",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000004",
      "name": "Read the chat event",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -220,
        400
      ],
      "parameters": {
        "jsCode": "// Works for both Chat app types: \"Workspace add-on\" events (body.chat) and classic Chat API events (body.type).\nconst body = $json.body || {};\nconst allowed = String($json.allowed_chat_users || '').toLowerCase().split(',').map((s) => s.trim()).filter(Boolean);\nconst addon = !!body.chat;\nlet kind, text, email;\nif (addon) {\n  const p = body.chat.messagePayload;\n  kind = p ? 'message' : body.chat.addedToSpacePayload ? 'added' : 'other';\n  text = p ? (p.message.argumentText || p.message.text || '') : '';\n  email = (body.chat.user && body.chat.user.email) || (p && p.message.sender && p.message.sender.email) || '';\n} else {\n  kind = body.type === 'MESSAGE' ? 'message' : body.type === 'ADDED_TO_SPACE' ? 'added' : 'other';\n  text = (body.message && (body.message.argumentText || body.message.text)) || '';\n  email = (body.user && body.user.email) || '';\n}\nconst wrap = (t) => (addon ? { hostAppDataAction: { chatDataAction: { createMessageAction: { message: { text: t } } } } } : { text: t });\nconst help = 'Write who and which month, for example \"Aoba, September\" or \"Blue Harbor の先月分\". I create a Gmail draft with the PDF; nothing is sent.';\nlet reply = null;\nif (kind !== 'message') reply = wrap(help);\nelse if (!allowed.includes(String(email).toLowerCase())) reply = wrap('Sorry, ' + (email || 'this account') + ' is not on the list of people who can create invoices.');\nelse if (!text.trim()) reply = wrap(help);\nreturn [{ json: { ok: !reply, reply, Request: text.trim(), source: 'chat', addon, sender: email } }];"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000005",
      "name": "Is it a request?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        0,
        400
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000006",
      "name": "Reply in Chat",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        3520,
        400
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json.reply) }}",
        "options": {}
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000007",
      "name": "Slack mention",
      "type": "n8n-nodes-base.slackTrigger",
      "typeVersion": 1,
      "position": [
        -660,
        760
      ],
      "parameters": {
        "trigger": [
          "app_mention"
        ],
        "watchWorkspace": true,
        "options": {}
      },
      "webhookId": "b8e4d0f2-5c3a-4f7b-8d9e-4a2c6f8b3d01"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000008",
      "name": "Slack settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -440,
        760
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": true,
        "assignments": {
          "assignments": [
            {
              "id": "s01",
              "name": "allowed_slack_users",
              "value": "U0123456789",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000009",
      "name": "Read the Slack message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -220,
        760
      ],
      "parameters": {
        "jsCode": "// The trigger may give the event itself or the whole envelope.\nconst ev = $json.event || $json;\nconst allowed = String($json.allowed_slack_users || '').split(',').map((s) => s.trim()).filter(Boolean);\nconst text = String(ev.text || '').replace(/<@[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim();\nconst base = { source: 'slack', channel: ev.channel, thread_ts: ev.thread_ts || ev.ts, sender: ev.user };\nlet reply = '';\nif (ev.bot_id) return [];\nif (!allowed.includes(ev.user)) reply = 'Sorry, you are not on the list of people who can create invoices.';\nelse if (!text) reply = 'Write who and which month, for example \"Aoba, September\" or \"Blue Harbor の先月分\". I create a Gmail draft with the PDF; nothing is sent.';\nreturn [{ json: { ok: !reply, text: reply, Request: text, ...base } }];"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000010",
      "name": "Is it a Slack request?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        0,
        760
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000011",
      "name": "Reply in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        3520,
        760
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.channel }}"
        },
        "messageType": "text",
        "text": "={{ $json.text }}",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "thread_ts": {
            "replyValues": {
              "thread_ts": "={{ $json.thread_ts }}"
            }
          }
        }
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000012",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "c00",
              "name": "request",
              "value": "={{ $json.Request }}",
              "type": "string"
            },
            {
              "id": "c0a",
              "name": "source",
              "value": "={{ $json.source || 'form' }}",
              "type": "string"
            },
            {
              "id": "c0b",
              "name": "addon",
              "value": "={{ $json.addon === true }}",
              "type": "boolean"
            },
            {
              "id": "c0c",
              "name": "slack_channel",
              "value": "={{ $json.channel || '' }}",
              "type": "string"
            },
            {
              "id": "c0d",
              "name": "slack_thread_ts",
              "value": "={{ $json.thread_ts || '' }}",
              "type": "string"
            },
            {
              "id": "c01",
              "name": "spreadsheet_id",
              "value": "PASTE_YOUR_SPREADSHEET_ID",
              "type": "string"
            },
            {
              "id": "c02",
              "name": "template_doc_id",
              "value": "PASTE_YOUR_TEMPLATE_DOC_ID",
              "type": "string"
            },
            {
              "id": "c03",
              "name": "seller_name",
              "value": "Example Logistics Co.",
              "type": "string"
            },
            {
              "id": "c04",
              "name": "seller_address",
              "value": "10-1 Example-cho, Shinagawa, Tokyo",
              "type": "string"
            },
            {
              "id": "c05",
              "name": "payment_details",
              "value": "Bank transfer: Example Bank, Head Office, Ordinary 1234567, Example Logistics Co.",
              "type": "string"
            },
            {
              "id": "c06",
              "name": "notes",
              "value": "Thank you for your business.",
              "type": "string"
            },
            {
              "id": "c07",
              "name": "currency",
              "value": "JPY",
              "type": "string"
            },
            {
              "id": "c08",
              "name": "currency_decimals",
              "value": 0,
              "type": "number"
            },
            {
              "id": "c09",
              "name": "number_locale",
              "value": "en-US",
              "type": "string"
            },
            {
              "id": "c10",
              "name": "tax_rate",
              "value": 10,
              "type": "number"
            },
            {
              "id": "c11",
              "name": "tax_rounding",
              "value": "floor",
              "type": "string"
            },
            {
              "id": "c12",
              "name": "invoice_prefix",
              "value": "INV-",
              "type": "string"
            },
            {
              "id": "c13",
              "name": "max_lines",
              "value": 20,
              "type": "number"
            },
            {
              "id": "c14",
              "name": "default_payment_terms_days",
              "value": 30,
              "type": "number"
            },
            {
              "id": "c15",
              "name": "timezone",
              "value": "Asia/Tokyo",
              "type": "string"
            },
            {
              "id": "c16",
              "name": "email_body",
              "value": "Dear [customer_name],\n\nPlease find attached invoice [invoice_no] for [period].\nTotal due: [total] by [due_date].\n\nBest regards,\n[seller_name]",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000013",
      "name": "Read the spreadsheet",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        440,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $json.spreadsheet_id }}/values:batchGet?ranges=Customers&ranges=Items&ranges=Invoices&valueRenderOption=UNFORMATTED_VALUE",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000014",
      "name": "Parse the sheets",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ],
      "parameters": {
        "jsCode": "// Turn the three tabs into lists of rows keyed by the header row.\nconst ranges = $json.valueRanges || [];\nconst table = (name) => {\n  const r = ranges.find((v) => String(v.range || '').replace(/^'/, '').startsWith(name + '!'));\n  const rows = (r && r.values) || [];\n  const head = (rows[0] || []).map((h) => String(h).trim());\n  return rows.slice(1)\n    .filter((row) => row.some((c) => String(c).trim() !== ''))\n    .map((row) => Object.fromEntries(head.map((h, i) => [h, row[i] ?? ''])));\n};\nconst customers = table('Customers');\nconst items = table('Items');\nconst invoices = table('Invoices');\nif (!customers.length) throw new Error('The Customers tab is empty or missing. Check spreadsheet_id in Config and the tab names (Customers, Items, Invoices).');\nreturn [{ json: { customers, items, invoices } }];"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000015",
      "name": "Claude Haiku 4.5",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [
        880,
        220
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-haiku-4-5-20251001",
          "cachedResultName": "Claude Haiku 4.5"
        },
        "options": {}
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000016",
      "name": "Read the request",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        880,
        0
      ],
      "parameters": {
        "promptType": "define",
        "text": "=Today: {{ $now.setZone($('Config').first().json.timezone).toFormat('yyyy-MM-dd') }}\nCustomers (id: name / other names):\n{{ $json.customers.map(c => c.customer_id + ': ' + c.name + (c.other_names ? ' / ' + c.other_names : '')).join('\\n') }}\n\nRequest:\n{{ $('Config').first().json.request }}",
        "messages": {
          "messageValues": [
            {
              "type": "SystemMessagePromptTemplate",
              "message": "You read a request to create an invoice. Do not compute dates or amounts.\nReturn only JSON with these keys:\n- \"customer_id\": the id from the customer list that the request names, or null\n- \"customer_words\": the exact words in the request that name the customer, or null\n- \"month\": the billing month the request asks for, as YYYY-MM, or null\n- \"month_words\": the exact words in the request that give the month (for example \"September\", \"9月\", \"先月\", \"2026-09\"), or null\nCopy customer_words and month_words exactly as they appear in the request."
            }
          ]
        }
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000017",
      "name": "Check the reading",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "jsCode": "// Code checks what Claude read against the request and the customer list. The month is computed here from the words.\nconst cfg = $('Config').first().json;\nconst request = String(cfg.request || '').trim();\nconst { customers } = $('Parse the sheets').first().json;\nlet r;\ntry { r = JSON.parse(String($json.text ?? '').match(/\\{[\\s\\S]*\\}/)[0]); }\ncatch (e) { return [{ json: { ok: false, problem: 'Could not read the AI answer as JSON. Try again or write the request more simply.' } }]; }\nconst norm = (s) => String(s ?? '').toLowerCase().normalize('NFKC').replace(/\\s+/g, ' ').trim();\nconst req = norm(request);\nconst problems = [];\nconst today = DateTime.now().setZone(cfg.timezone);\n\nconst cust = customers.find((c) => String(c.customer_id).trim() === r.customer_id);\nif (!r.customer_id) problems.push('The request does not name a customer from the Customers tab.');\nelse if (!cust) problems.push('\"' + r.customer_id + '\" is not a customer_id in the Customers tab.');\nelse {\n  const words = norm(r.customer_words);\n  const names = [cust.name, ...String(cust.other_names || '').split(',')].map(norm).filter(Boolean);\n  if (!words || !req.includes(words)) problems.push('The customer words \"' + (r.customer_words ?? '') + '\" are not in the request.');\n  else if (!names.some((nm) => nm.includes(words) || words.includes(nm))) problems.push('\"' + r.customer_words + '\" does not match ' + cust.name + ' or its other names.');\n}\n\nconst monthNames = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];\nconst pick = (mm) => { let y = today.year; if (mm > today.month) y -= 1; return y + '-' + String(mm).padStart(2, '0'); };\nfunction monthFromWords(w) {\n  const s = norm(w);\n  let m;\n  if ((m = s.match(/(\\d{4})\\s*[-\\/年.]\\s*(\\d{1,2})/))) return m[1] + '-' + m[2].padStart(2, '0');\n  if ((m = s.match(/(\\d{1,2})\\s*月/))) return pick(Number(m[1]));\n  for (let i = 0; i < 12; i++) if (s.includes(monthNames[i]) || new RegExp('\\\\b' + monthNames[i].slice(0, 3) + '\\\\b').test(s)) return pick(i + 1);\n  if (/先月|前月|last month/.test(s)) return today.minus({ months: 1 }).toFormat('yyyy-MM');\n  if (/今月|this month/.test(s)) return today.toFormat('yyyy-MM');\n  return null;\n}\nif (!r.month_words) problems.push('The request does not say which month. Write it, for example \"September\", \"9月\" or \"2026-09\".');\nelse if (!req.includes(norm(r.month_words))) problems.push('The month words \"' + r.month_words + '\" are not in the request.');\nelse {\n  const m = monthFromWords(r.month_words);\n  if (!m) problems.push('Could not read a month from \"' + r.month_words + '\".');\n  else if (m !== r.month) problems.push('The AI read the month as ' + r.month + ', but \"' + r.month_words + '\" means ' + m + '.');\n  else if (m > today.toFormat('yyyy-MM')) problems.push(m + ' is in the future.');\n}\nreturn [{ json: { ok: problems.length === 0, problem: problems.join(' '), customer_id: r.customer_id, month: r.month, read: r } }];"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000018",
      "name": "Reading OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000019",
      "name": "Build the invoice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        -100
      ],
      "parameters": {
        "jsCode": "// Code finds the lines, computes every amount and date, and prepares the text for the template.\nconst cfg = $('Config').first().json;\nconst { customers, items, invoices } = $('Parse the sheets').first().json;\nconst { customer_id, month } = $json;\nconst cust = customers.find((c) => String(c.customer_id).trim() === customer_id);\nconst toMonth = (v) => {\n  if (typeof v === 'number') return new Date(Date.UTC(1899, 11, 30) + v * 86400000).toISOString().slice(0, 7);\n  const m = String(v).trim().match(/^(\\d{4})[-\\/](\\d{1,2})/);\n  return m ? m[1] + '-' + m[2].padStart(2, '0') : String(v).trim();\n};\nconst num = (v) => (typeof v === 'number' ? v : Number(String(v).replace(/[,\\s¥$€]/g, '')));\nconst lines = items.filter((i) => String(i.customer_id).trim() === customer_id && toMonth(i.month) === month);\nconst invoice_no = cfg.invoice_prefix + month.replace('-', '') + '-' + customer_id;\nconst problems = [];\nif (!lines.length) problems.push('No lines for ' + cust.name + ' in ' + month + ' in the Items tab.');\nif (lines.length > cfg.max_lines) problems.push(lines.length + ' lines for ' + month + ', but max_lines in Config is ' + cfg.max_lines + '. Check the Items tab, or raise max_lines.');\nconst bad = lines.filter((l) => !Number.isFinite(num(l.quantity)) || !Number.isFinite(num(l.unit_price)) || String(l.quantity).trim() === '' || String(l.unit_price).trim() === '');\nif (bad.length) problems.push('Quantity or unit price is not a number for: ' + bad.map((b) => b.description).join(', ') + '.');\nconst prev = invoices.find((x) => String(x.invoice_no).trim() === invoice_no && String(x.status).trim().toLowerCase() !== 'void');\nif (prev) problems.push(invoice_no + ' was already created (' + prev.created_at + '). To create it again, set its status to \"void\" in the Invoices tab.');\nif (problems.length) return [{ json: { ok: false, problem: problems.join(' ') } }];\n\nconst dec = Number(cfg.currency_decimals);\nconst roundTo = (x, mode) => { const f = 10 ** dec; const v = x * f; const r = mode === 'floor' ? Math.floor(v + 1e-9) : mode === 'ceil' ? Math.ceil(v - 1e-9) : Math.round(v); return r / f; };\nconst fmt = (x) => new Intl.NumberFormat(cfg.number_locale, { style: 'currency', currency: cfg.currency, minimumFractionDigits: dec, maximumFractionDigits: dec }).format(x);\nconst rows = lines.map((l) => { const q = num(l.quantity); const p = num(l.unit_price); return { description: String(l.description), q, p, amount: roundTo(q * p, 'round') }; });\nconst subtotal = roundTo(rows.reduce((s, x) => s + x.amount, 0), 'round');\nconst tax = roundTo(subtotal * Number(cfg.tax_rate) / 100, cfg.tax_rounding);\nconst total = roundTo(subtotal + tax, 'round');\nconst today = DateTime.now().setZone(cfg.timezone);\nconst terms = Number(cust.payment_terms_days) || Number(cfg.default_payment_terms_days);\nconst due = today.plus({ days: terms });\nconst start = DateTime.fromFormat(month, 'yyyy-MM', { zone: cfg.timezone });\nconst period = start.toFormat('yyyy-MM-dd') + ' to ' + start.endOf('month').toFormat('yyyy-MM-dd');\nconst values = {\n  invoice_no, issue_date: today.toFormat('yyyy-MM-dd'), due_date: due.toFormat('yyyy-MM-dd'),\n  customer_name: cust.name, customer_address: cust.address, period,\n  subtotal: fmt(subtotal), tax_rate: cfg.tax_rate + '%', tax: fmt(tax), total: fmt(total),\n  seller_name: cfg.seller_name, seller_address: cfg.seller_address, payment_details: cfg.payment_details, notes: cfg.notes,\n};\n// One line per item; the newlines become separate paragraphs in the document.\nvalues.lines = rows.map((x) => x.description + '   ' + x.q + ' x ' + fmt(x.p) + ' = ' + fmt(x.amount)).join('\\n');\nconst requests = Object.entries(values).map(([k, v]) => ({ replaceAllText: { containsText: { text: '{{' + k + '}}', matchCase: true }, replaceText: String(v ?? '') } }));\nconst body = String(cfg.email_body).replace(/\\[(\\w+)\\]/g, (m, k) => (k in values ? values[k] : m));\nreturn [{ json: {\n  ok: true, invoice_no, customer_id, month, email: cust.email, values, requests,\n  amounts: { subtotal, tax, total, lines: rows.length },\n  subject: 'Invoice ' + invoice_no + ' from ' + cfg.seller_name, body,\n  doc_name: 'Invoice ' + invoice_no + ' ' + cust.name,\n} }];"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000020",
      "name": "Invoice OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        -100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000021",
      "name": "Copy the template",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1980,
        -200
      ],
      "parameters": {
        "resource": "file",
        "operation": "copy",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Config').first().json.template_doc_id }}"
        },
        "name": "={{ $json.doc_name }}",
        "sameFolder": true,
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000022",
      "name": "Fill in the copy",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        2200,
        -200
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://docs.googleapis.com/v1/documents/{{ $json.id }}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDocsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ requests: $('Build the invoice').first().json.requests }) }}",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000023",
      "name": "Download as PDF",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        2420,
        -200
      ],
      "parameters": {
        "resource": "file",
        "operation": "download",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Copy the template').first().json.id }}"
        },
        "options": {
          "binaryPropertyName": "data",
          "fileName": "={{ $('Build the invoice').first().json.invoice_no + '.pdf' }}",
          "googleFileConversion": {
            "conversion": {
              "docsToFormat": "application/pdf"
            }
          }
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000024",
      "name": "Create Gmail draft",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2640,
        -200
      ],
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $('Build the invoice').first().json.subject }}",
        "emailType": "text",
        "message": "={{ $('Build the invoice').first().json.body }}",
        "options": {
          "sendTo": "={{ $('Build the invoice').first().json.email }}",
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "data"
              }
            ]
          }
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000025",
      "name": "Log the invoice",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        2860,
        -200
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{ $('Config').first().json.spreadsheet_id }}/values/Invoices!A1:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ values: [[ $('Build the invoice').first().json.invoice_no, $('Build the invoice').first().json.customer_id, $('Build the invoice').first().json.month, $('Build the invoice').first().json.amounts.subtotal, $('Build the invoice').first().json.amounts.tax, $('Build the invoice').first().json.amounts.total, $('Build the invoice').first().json.values.due_date, 'https://docs.google.com/document/d/' + $('Copy the template').first().json.id + '/edit', $json.id, $now.setZone($('Config').first().json.timezone).toISO(), 'draft' ]] }) }}",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000026",
      "name": "Answer where asked",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        3080,
        -200
      ],
      "parameters": {
        "mode": "expression",
        "numberOutputs": 3,
        "output": "={{ ({ form: 0, chat: 1, slack: 2 })[$('Config').first().json.source] ?? 0 }}"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000027",
      "name": "Problem: answer where asked",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        3080,
        160
      ],
      "parameters": {
        "mode": "expression",
        "numberOutputs": 3,
        "output": "={{ ({ form: 0, chat: 1, slack: 2 })[$('Config').first().json.source] ?? 0 }}"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000028",
      "name": "Write the Chat reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        400
      ],
      "parameters": {
        "jsCode": "// The document link is a button, not text: a URL in the text gets a preview made before the copy was filled in.\nconst addon = $('Config').first().json.addon;\nlet message;\nif ($json.problem) message = { text: 'Nothing was created. ' + $json.problem };\nelse {\n  const b = $('Build the invoice').first().json;\n  const url = 'https://docs.google.com/document/d/' + $('Copy the template').all(0)[0].json.id + '/edit';\n  message = {\n    text: 'Draft created: ' + b.invoice_no + '\\nTotal ' + b.values.total + ' (' + b.amounts.lines + ' lines). The Gmail draft to ' + b.email + ' has the PDF attached. Check it and send it yourself.',\n    cardsV2: [{ cardId: 'invoice', card: { sections: [{ widgets: [{ buttonList: { buttons: [{ text: 'Open the document', onClick: { openLink: { url } } }] } }] }] } }],\n  };\n}\nconst reply = addon ? { hostAppDataAction: { chatDataAction: { createMessageAction: { message } } } } : message;\nreturn [{ json: { reply } }];"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000029",
      "name": "Write the Slack reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        760
      ],
      "parameters": {
        "jsCode": "// Reply in the thread of the request.\nconst c = $('Config').first().json;\nlet text;\nif ($json.problem) text = 'Nothing was created. ' + $json.problem;\nelse {\n  const b = $('Build the invoice').first().json;\n  const url = 'https://docs.google.com/document/d/' + $('Copy the template').all(0)[0].json.id + '/edit';\n  text = '*Draft created: ' + b.invoice_no + '*\\nTotal ' + b.values.total + ' (' + b.amounts.lines + ' lines). The Gmail draft to ' + b.email + ' has the PDF attached. Check it and send it yourself.\\n<' + url + '|Open the document>';\n}\nreturn [{ json: { channel: c.slack_channel, thread_ts: c.slack_thread_ts, text } }];"
      }
    },
    {
      "id": "c3000000-0000-4000-8000-000000000030",
      "name": "Done",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        3300,
        -300
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "={{ 'Draft created: ' + $('Build the invoice').first().json.invoice_no }}",
        "completionMessage": "={{ 'Total ' + $('Build the invoice').first().json.values.total + ' (' + $('Build the invoice').first().json.amounts.lines + ' lines). The Gmail draft to ' + $('Build the invoice').first().json.email + ' has the PDF attached. Check it and send it yourself. Document: https://docs.google.com/document/d/' + $('Copy the template').all(0)[0].json.id + '/edit' }}"
      },
      "webhookId": "0b9e2d41-7c3a-4f6b-8e2d-1a5c9f3b7d20"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000031",
      "name": "Show the problem",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        3300,
        120
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "Nothing was created",
        "completionMessage": "={{ $json.problem }}"
      },
      "webhookId": "9f4a1c2e-5b7d-4e8a-b3c6-2d9e0f1a4b57"
    },
    {
      "id": "c3000000-0000-4000-8000-000000000032",
      "name": "Explain the failure",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        160
      ],
      "parameters": {
        "jsCode": "// The error comes from the node that failed after its retries.\nconst labels = {\n  'Read the spreadsheet': 'reading the spreadsheet',\n  'Copy the template': 'copying the template',\n  'Fill in the copy': 'filling in the document',\n  'Download as PDF': 'making the PDF',\n  'Create Gmail draft': 'creating the Gmail draft',\n  'Log the invoice': 'adding the row to Invoices',\n};\nconst node = $prevNode.name;\nconst err = $json.error && ($json.error.message || $json.error);\nconst leftover = ['Fill in the copy', 'Download as PDF', 'Create Gmail draft', 'Log the invoice'].includes(node);\nreturn [{ json: { problem: 'Google returned an error while ' + (labels[node] || node) + (err ? ' (' + String(err).slice(0, 200) + ')' : '') + '. '\n  + 'The invoice was not added to the Invoices tab, so you can ask again once the cause is fixed.'\n  + (leftover ? ' A half-made copy of the template is left in Google Drive' + (node === 'Log the invoice' ? ' and a Gmail draft was created' : '') + '; delete them.' : '') } }];"
      }
    },
    {
      "parameters": {
        "content": "## Create invoice drafts from a sentence, without letting AI do the math\nAsk on a form, in Google Chat or in Slack: \"Aoba, September\" or \"Blue Harbor の先月分\". **Claude only reads who and which month.** **Code checks that reading against your words and computes every amount and date** from your Google Sheet. Then a copy of your Google Docs template is filled in, saved as PDF and attached to a **Gmail draft. Nothing is sent**: a person opens the draft and sends it.\n\n### It stops instead of guessing\n- a customer not in the Customers tab, or words that do not match its name\n- no month, or a month Claude read differently from the words\n- no lines, too many lines, or a quantity or price that is not a number\n- an invoice number that was already created (set its status to `void` to redo it)\n- a Google error that is still there after 3 tries: you are told which step failed\n\n### Setup\n1. Credentials: Google Sheets, Google Drive, Google Docs, Gmail (OAuth2) and Anthropic; Slack only if you use it.\n2. Make a spreadsheet with tabs **Customers**, **Items**, **Invoices** and a Google Docs template (columns and placeholders in the description).\n3. In **Config**, paste the spreadsheet ID and template doc ID and write your company details.\n4. Try the form first. For Google Chat or Slack, see section 1. Delete the entry you do not use.",
        "height": 1160,
        "width": 580,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1360,
        -200
      ],
      "id": "c3000000-0000-4000-9000-000000000032",
      "name": "Overview"
    },
    {
      "parameters": {
        "content": "## 1. The request comes in\n**Form**: open the form URL and write the request.\n**Google Chat** (optional, Workspace): create a Chat app in Google Cloud (Chat API > Configuration), choose **HTTP endpoint URL** and paste the production URL of **Google Chat event**. Change its path to a long random string and list who may use it in **Chat settings**. Chat waits 30 seconds; my runs took 10 to 15 seconds.\n**Slack** (optional): in your Slack app add the bot scopes app_mentions:read and chat:write, turn on Event Subscriptions with the production URL of **Slack mention**, subscribe to app_mention, and list member IDs in **Slack settings**. Mention the app in a channel it has joined; the answer goes to the thread.",
        "height": 1160,
        "width": 1040,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -720,
        -200
      ],
      "id": "c3000000-0000-4000-9000-000000000033",
      "name": "Section 1"
    },
    {
      "parameters": {
        "content": "## 2. Claude reads, code checks\nOne read of the three tabs. Claude returns the customer ID and month plus the exact words it used. Code checks that those words are in the request and match the customer's name or other names, and computes the month itself (\"9月\", \"September\", \"last month\", \"2026-09\").",
        "height": 580,
        "width": 1020,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        400,
        -200
      ],
      "id": "c3000000-0000-4000-9000-000000000034",
      "name": "Section 2"
    },
    {
      "parameters": {
        "content": "## 3. Code computes\nLines, subtotal, tax (rounding in Config), total, due date from payment terms, and the invoice number. Duplicates stop here.",
        "height": 380,
        "width": 420,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1480,
        -280
      ],
      "id": "c3000000-0000-4000-9000-000000000035",
      "name": "Section 3"
    },
    {
      "parameters": {
        "content": "## 4. Document, PDF, draft, log\nCopy the template, replace the {{placeholders}}, export as PDF, create the Gmail draft, add a row to the Invoices tab. Each Google call is retried 3 times; if it still fails, **Explain the failure** says which step stopped.",
        "height": 720,
        "width": 1060,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1940,
        -380
      ],
      "id": "c3000000-0000-4000-9000-000000000036",
      "name": "Section 4"
    },
    {
      "parameters": {
        "content": "## 5. The answer\nAnswered where the request came from: a result page on the form, a message with an \"Open the document\" button in Google Chat (both Chat app types), or a reply in the Slack thread.",
        "height": 1440,
        "width": 680,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3020,
        -480
      ],
      "id": "c3000000-0000-4000-9000-000000000037",
      "name": "Section 5"
    }
  ],
  "connections": {
    "Google Chat event": {
      "main": [
        [
          {
            "node": "Chat settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat settings": {
      "main": [
        [
          {
            "node": "Read the chat event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the chat event": {
      "main": [
        [
          {
            "node": "Is it a request?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is it a request?": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply in Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack mention": {
      "main": [
        [
          {
            "node": "Slack settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack settings": {
      "main": [
        [
          {
            "node": "Read the Slack message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the Slack message": {
      "main": [
        [
          {
            "node": "Is it a Slack request?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is it a Slack request?": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request form": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Read the spreadsheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the spreadsheet": {
      "main": [
        [
          {
            "node": "Parse the sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse the sheets": {
      "main": [
        [
          {
            "node": "Read the request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Haiku 4.5": {
      "ai_languageModel": [
        [
          {
            "node": "Read the request",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Read the request": {
      "main": [
        [
          {
            "node": "Check the reading",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the reading": {
      "main": [
        [
          {
            "node": "Reading OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reading OK?": {
      "main": [
        [
          {
            "node": "Build the invoice",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Problem: answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build the invoice": {
      "main": [
        [
          {
            "node": "Invoice OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Invoice OK?": {
      "main": [
        [
          {
            "node": "Copy the template",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Problem: answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Copy the template": {
      "main": [
        [
          {
            "node": "Fill in the copy",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fill in the copy": {
      "main": [
        [
          {
            "node": "Download as PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download as PDF": {
      "main": [
        [
          {
            "node": "Create Gmail draft",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Gmail draft": {
      "main": [
        [
          {
            "node": "Log the invoice",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log the invoice": {
      "main": [
        [
          {
            "node": "Answer where asked",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer where asked": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the Chat reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the Slack reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Problem: answer where asked": {
      "main": [
        [
          {
            "node": "Show the problem",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the Chat reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the Slack reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the Chat reply": {
      "main": [
        [
          {
            "node": "Reply in Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the Slack reply": {
      "main": [
        [
          {
            "node": "Reply in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Explain the failure": {
      "main": [
        [
          {
            "node": "Problem: answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}