{
  "name": "Lumeor | Accounts receivable collections (QuickBooks + Gmail)",
  "nodes": [
    {
      "parameters": {
        "content": "## Accounts receivable collections (QuickBooks Online + Gmail)\n\n**Daily run (top)**\n1. Weekdays at 8am, pulls open invoices from QuickBooks Online.\n2. Applies the cadence table (segment + days past due = action) from Google Sheets.\n3. Skips customers with an active promise to pay, a dispute in the last 30 days, or a reminder in the last 7 days.\n4. Claude drafts each email from the real invoice list. Code checks it and falls back to a fixed template if anything is off.\n5. Creates Gmail drafts for a person to send, and Slack tasks for key accounts.\n\n**Replies (bottom)**\n6. Claude classifies replies in the AR mailbox (paid, promise, dispute, copy/W-9, wrong contact, out of office) and posts them to Slack. Promises and disputes pause reminders.\n\n**Setup**\n1. Credentials: QuickBooks Online OAuth2, Google Sheets, Gmail OAuth2, Anthropic, Slack.\n2. Sheet tabs: \"Cadence\" (segment, min_days, max_days, action, tone) and \"AR log\" (logged_at, flow, customer, customer_email, invoice_numbers, action, label, promise_date, detail). Replace REPLACE_WITH_SHEET_ID.\n3. Edit the constants at the top of **Plan reminders** and the signature in **Check draft**.\n4. Replace REPLACE_WITH_AR_INBOX_ADDRESS and REPLACE_WITH_AR_CHANNEL.\n\nModel: claude-opus-5-5. claude-haiku-5-5 is a cheaper option for reply classification.\n\nTemplate by Lumeor Studio, https://www.lumeor.studio/guides/automate-accounts-receivable-collections/",
        "height": 820,
        "width": 520
      },
      "id": "ee02f1cf-b962-46b9-91a3-438e59006bd2",
      "name": "Template notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -620,
        -320
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "id": "0f0c6a83-f615-4355-9789-22f4e8dd001a",
      "name": "Weekdays 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "resource": "invoice",
        "operation": "getAll",
        "returnAll": true,
        "filters": {
          "query": "WHERE Balance > '0'"
        }
      },
      "id": "bb178aa3-90f4-48c3-84cd-82728785bfff",
      "name": "Get open invoices",
      "type": "n8n-nodes-base.quickbooks",
      "typeVersion": 1,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Cadence"
        },
        "options": {}
      },
      "id": "bf9768b8-a0c6-4689-b90a-c20fee2c989b",
      "name": "Read cadence",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        440,
        0
      ],
      "executeOnce": true,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "AR log"
        },
        "options": {}
      },
      "id": "ce9a7b95-787d-4573-97c6-fd9a0e4829a4",
      "name": "Read AR log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        0
      ],
      "executeOnce": true,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Groups open invoices by customer, applies the cadence table and skips anyone with an\n// active promise to pay, an open dispute or a recent touch. Builds one Claude draft request per customer.\nconst COMPANY_NAME = 'REPLACE_WITH_COMPANY_NAME';\nconst PAYMENT_LINK = 'REPLACE_WITH_PAYMENT_LINK';\nconst KEY_ACCOUNT_IDS = ['REPLACE_WITH_QBO_CUSTOMER_ID']; // QuickBooks customer IDs handled by account owners\nconst SMALL_BALANCE_UNDER = 500;\nconst MIN_DAYS_BETWEEN_TOUCHES = 7;\n\nconst today = new Date().toISOString().slice(0, 10);\nconst daysBetween = (a, b) => Math.floor((Date.parse(b) - Date.parse(a)) / 86400000);\nconst invoices = $('Get open invoices').all().map((i) => i.json).filter((inv) => Number(inv.Balance) > 0);\nconst cadence = $('Read cadence').all().map((i) => i.json).filter((r) => r.segment && r.action);\nconst log = $('Read AR log').all().map((i) => i.json).filter((r) => r.customer_email);\n\nconst byCustomer = {};\nfor (const inv of invoices) {\n  const id = (inv.CustomerRef || {}).value;\n  if (!byCustomer[id]) byCustomer[id] = { customerId: id, customer: (inv.CustomerRef || {}).name || id, email: '', invoices: [] };\n  if (!byCustomer[id].email && inv.BillEmail && inv.BillEmail.Address) byCustomer[id].email = inv.BillEmail.Address.split(',')[0].trim();\n  byCustomer[id].invoices.push({ number: inv.DocNumber || inv.Id, dueDate: inv.DueDate, balance: Number(inv.Balance), daysPastDue: daysBetween(inv.DueDate, today) });\n}\n\nconst schema = {\n  type: 'object',\n  additionalProperties: false,\n  required: ['subject', 'body'],\n  properties: {\n    subject: { type: 'string' },\n    body: { type: 'string', description: 'Plain text email body. Must include the literal token [PAYMENT_LINK] once.' },\n  },\n};\n\nconst out = [];\nfor (const c of Object.values(byCustomer)) {\n  const email = c.email.toLowerCase();\n  const history = log.filter((r) => String(r.customer_email).toLowerCase() === email);\n  const promise = history.find((r) => r.label === 'promise_to_pay' && r.promise_date && r.promise_date >= today);\n  const dispute = history.find((r) => r.label === 'dispute' && daysBetween(String(r.logged_at).slice(0, 10), today) <= 30);\n  const recent = history.find((r) => r.flow === 'reminder' && daysBetween(String(r.logged_at).slice(0, 10), today) < MIN_DAYS_BETWEEN_TOUCHES);\n  if (promise || dispute || recent) continue;\n\n  const total = c.invoices.reduce((s, i) => s + i.balance, 0);\n  const maxPastDue = Math.max(...c.invoices.map((i) => i.daysPastDue));\n  const segment = KEY_ACCOUNT_IDS.includes(String(c.customerId)) ? 'key' : total < SMALL_BALANCE_UNDER ? 'small' : 'standard';\n  const rule = cadence.find((r) => r.segment === segment && maxPastDue >= Number(r.min_days) && maxPastDue <= Number(r.max_days));\n  if (!rule || rule.action === 'none') continue;\n\n  const list = c.invoices.map((i) => 'Invoice ' + i.number + ': $' + i.balance.toFixed(2) + ', due ' + i.dueDate).join('\\n');\n  const logRow = { flow: 'reminder', customer: c.customer, customer_email: email, invoice_numbers: c.invoices.map((i) => i.number).join(', '), action: rule.action, label: '', promise_date: '', detail: segment + ', ' + maxPastDue + ' days past due, $' + total.toFixed(2) };\n\n  if (rule.action === 'owner_task' || !email) {\n    out.push({ json: {\n      route: 'owner_task',\n      log: { ...logRow, action: 'owner_task', detail: logRow.detail + (email ? '' : ', no billing email') },\n      slackText: ':bust_in_silhouette: *Collections task for the account owner*\\n' + c.customer + ' (' + segment + ') owes $' + total.toFixed(2) + ', ' + maxPastDue + ' days past due.\\n' + list + (email ? '' : '\\nNo billing email on file in QuickBooks.'),\n    }, pairedItem: { item: 0 } });\n    continue;\n  }\n\n  const prompt = 'Write a short accounts receivable email from ' + COMPANY_NAME + ' to ' + c.customer + '.\\n' +\n    'Tone: ' + (rule.tone || rule.action) + '. Stage: ' + rule.action + '.\\n' +\n    'Open invoices (use these exact numbers and amounts, do not add any others):\\n' + list + '\\n' +\n    'Total open: $' + total.toFixed(2) + '.\\n' +\n    'Rules: under 120 words; list each invoice number; include the token [PAYMENT_LINK] once where the reader can pay; ' +\n    'invite them to reply if they already paid or if something is wrong; no threats of fees, collections agencies or legal action; ' +\n    'no sign-off name (the signature is added later).';\n\n  out.push({ json: {\n    route: 'email',\n    customer: c.customer,\n    email,\n    invoiceNumbers: c.invoices.map((i) => String(i.number)),\n    total,\n    list,\n    paymentLink: PAYMENT_LINK,\n    companyName: COMPANY_NAME,\n    log: logRow,\n    claudeRequest: {\n      model: 'claude-opus-5-5',\n      max_tokens: 2000,\n      messages: [{ role: 'user', content: prompt }],\n      output_config: { format: { type: 'json_schema', schema } },\n    },\n  }, pairedItem: { item: 0 } });\n}\nreturn out;\n"
      },
      "id": "094ffaa5-7279-4c0e-9172-5ed13899511f",
      "name": "Plan reminders",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "875d3caa-cb02-485c-a1d1-a64aa4c441ea",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "email",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "email"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cb76f6bc-cfcc-47f9-93bd-5a5b42c495c0",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "owner_task",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "owner_task"
            }
          ]
        },
        "options": {}
      },
      "id": "d770a741-e165-48bd-b437-5675a9c1abd9",
      "name": "Email or owner task",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1100,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "anthropicApi",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.claudeRequest) }}",
        "options": {
          "timeout": 180000
        }
      },
      "id": "d98d38dc-1b4f-4008-9804-12a6bf08c570",
      "name": "Claude: draft reminder",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        -100
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Validates each AI draft. Anything that fails falls back to a fixed, reviewed template.\n\n// Reads a Messages API response. Thinking blocks may come first, so find the text block.\nfunction readClaude(resp) {\n  if (!resp || resp.error) {\n    const msg = resp && resp.error ? (resp.error.message || JSON.stringify(resp.error)) : 'empty response';\n    return { ok: false, reason: 'api_error: ' + String(msg).slice(0, 300) };\n  }\n  if (resp.stop_reason === 'refusal') {\n    const cat = resp.stop_details && resp.stop_details.category ? ' (' + resp.stop_details.category + ')' : '';\n    return { ok: false, reason: 'model_refusal' + cat };\n  }\n  if (resp.stop_reason === 'max_tokens') return { ok: false, reason: 'max_tokens: output was cut off, raise max_tokens' };\n  const block = (resp.content || []).find((b) => b.type === 'text');\n  if (!block) return { ok: false, reason: 'no_text_block_in_response' };\n  try {\n    return { ok: true, data: JSON.parse(block.text) };\n  } catch (e) {\n    return { ok: false, reason: 'invalid_json: ' + e.message };\n  }\n}\n\nconst BANNED = ['legal action', 'collections agency', 'collection agency', 'late fee', 'lawsuit', 'attorney'];\nconst SIGNATURE = 'REPLACE_WITH_AR_SIGNATURE';\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const ctx = $('Plan reminders').itemMatching(i).json;\n  const r = readClaude(items[i].json);\n  const problems = [];\n  let subject = '';\n  let body = '';\n  if (!r.ok) problems.push(r.reason);\n  else {\n    subject = String(r.data.subject || '').trim();\n    body = String(r.data.body || '').trim();\n    const missing = ctx.invoiceNumbers.filter((n) => !body.includes(n));\n    if (missing.length) problems.push('missing_invoice_numbers: ' + missing.join(', '));\n    if (!body.includes('[PAYMENT_LINK]')) problems.push('missing_payment_link_token');\n    const lower = (subject + ' ' + body).toLowerCase();\n    const hit = BANNED.filter((b) => lower.includes(b));\n    if (hit.length) problems.push('banned_phrases: ' + hit.join(', '));\n    if (body.split(/\\s+/).length > 180) problems.push('too_long');\n  }\n  if (problems.length) {\n    subject = 'Open invoices from ' + ctx.companyName;\n    body = 'Hello,\\n\\nA quick reminder that the following invoices are open on your account:\\n\\n' + ctx.list +\n      '\\n\\nYou can pay here: [PAYMENT_LINK]\\n\\nIf you have already paid, or something looks wrong, just reply to this email and we will sort it out.';\n  }\n  body = body.replace('[PAYMENT_LINK]', ctx.paymentLink) + '\\n\\n' + SIGNATURE;\n  out.push({\n    json: {\n      email: ctx.email,\n      subject,\n      body,\n      log: { ...ctx.log, detail: ctx.log.detail + (problems.length ? ' | fallback template: ' + problems.join('; ') : ' | AI draft') },\n    },\n    pairedItem: { item: i },\n  });\n}\nreturn out;\n"
      },
      "id": "895b20cf-403d-49c1-a6b4-21526273b8a0",
      "name": "Check draft",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        -100
      ]
    },
    {
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {
          "sendTo": "={{ $json.email }}"
        }
      },
      "id": "ebc2b0b9-d0bc-476e-b2c5-03dceed79b29",
      "name": "Create Gmail draft",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1760,
        -200
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "AR log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "logged_at": "={{ $now.toISO() }}",
            "flow": "={{ $json.log.flow }}",
            "customer": "={{ $json.log.customer }}",
            "customer_email": "={{ $json.log.customer_email }}",
            "invoice_numbers": "={{ $json.log.invoice_numbers }}",
            "action": "={{ $json.log.action }}",
            "label": "={{ $json.log.label }}",
            "promise_date": "={{ $json.log.promise_date }}",
            "detail": "={{ $json.log.detail }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "logged_at",
              "displayName": "logged_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "flow",
              "displayName": "flow",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "customer",
              "displayName": "customer",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_email",
              "displayName": "customer_email",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "invoice_numbers",
              "displayName": "invoice_numbers",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "action",
              "displayName": "action",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "label",
              "displayName": "label",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "promise_date",
              "displayName": "promise_date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "detail",
              "displayName": "detail",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "7b78daba-c7ef-4748-a6d4-69617b27542f",
      "name": "Append to AR log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1980,
        100
      ]
    },
    {
      "parameters": {
        "authentication": "accessToken",
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#REPLACE_WITH_AR_CHANNEL"
        },
        "messageType": "text",
        "text": "={{ $json.slackText }}",
        "otherOptions": {
          "includeLinkToWorkflow": true
        }
      },
      "id": "f7660092-bac7-4590-a327-abfa796d2b41",
      "name": "Post to AR channel",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1980,
        300
      ]
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "to:REPLACE_WITH_AR_INBOX_ADDRESS -from:me in:inbox",
          "readStatus": "unread"
        },
        "options": {}
      },
      "id": "cd2f3445-c8a3-421f-84f1-d746743f515f",
      "name": "AR mailbox reply",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        420
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "anthropicApi",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'claude-opus-5-5', max_tokens: 1500, messages: [{ role: 'user', content: 'Classify this reply to an accounts receivable reminder. Today is ' + $now.toISODate() + '. Extract invoice numbers and any promised payment date (resolve relative dates such as next Friday).\\n\\nSubject: ' + ($json.subject || '') + '\\n\\n' + String($json.text || '').slice(0, 20000) }], output_config: { effort: 'low', format: { type: 'json_schema', schema: {'type':'object','additionalProperties':false,'required':['label','promise_date','invoice_numbers','summary'],'properties':{'label':{'type':'string','enum':['paid_with_remittance','promise_to_pay','dispute','request_copy_or_w9','wrong_contact','out_of_office','other']},'promise_date':{'type':'string','description':'YYYY-MM-DD if the customer promised a payment date, else empty string'},'invoice_numbers':{'type':'array','items':{'type':'string'}},'summary':{'type':'string','description':'One sentence'}}} } } }) }}",
        "options": {
          "timeout": 180000
        }
      },
      "id": "c8133157-0783-4ae3-be5c-7de7603faff2",
      "name": "Claude: classify reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        420
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Turns the reply classification into a Slack note for AR and a log row.\n\n// Reads a Messages API response. Thinking blocks may come first, so find the text block.\nfunction readClaude(resp) {\n  if (!resp || resp.error) {\n    const msg = resp && resp.error ? (resp.error.message || JSON.stringify(resp.error)) : 'empty response';\n    return { ok: false, reason: 'api_error: ' + String(msg).slice(0, 300) };\n  }\n  if (resp.stop_reason === 'refusal') {\n    const cat = resp.stop_details && resp.stop_details.category ? ' (' + resp.stop_details.category + ')' : '';\n    return { ok: false, reason: 'model_refusal' + cat };\n  }\n  if (resp.stop_reason === 'max_tokens') return { ok: false, reason: 'max_tokens: output was cut off, raise max_tokens' };\n  const block = (resp.content || []).find((b) => b.type === 'text');\n  if (!block) return { ok: false, reason: 'no_text_block_in_response' };\n  try {\n    return { ok: true, data: JSON.parse(block.text) };\n  } catch (e) {\n    return { ok: false, reason: 'invalid_json: ' + e.message };\n  }\n}\n\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const mail = $('AR mailbox reply').itemMatching(i).json;\n  const from = ((mail.from && mail.from.value && mail.from.value[0]) || {}).address || '';\n  const r = readClaude(items[i].json);\n  const c = r.ok ? r.data : { label: 'unclassified', promise_date: '', invoice_numbers: [], summary: 'Could not classify: ' + r.reason };\n  const next = {\n    paid_with_remittance: 'Match the payment in QuickBooks and confirm.',\n    promise_to_pay: 'Reminders pause until ' + (c.promise_date || 'the promised date') + '.',\n    dispute: 'Reminders pause for 30 days. Create a task to resolve it.',\n    request_copy_or_w9: 'Send the requested document.',\n    wrong_contact: 'Update the billing email in QuickBooks.',\n    out_of_office: 'No action needed.',\n    other: 'Read and reply.',\n    unclassified: 'Read and reply.',\n  }[c.label] || 'Read and reply.';\n  out.push({\n    json: {\n      slackText: ':envelope_with_arrow: *AR reply: ' + c.label + '*\\nFrom: ' + from + '\\nSubject: ' + (mail.subject || '') +\n        '\\n' + c.summary + (c.invoice_numbers.length ? '\\nInvoices: ' + c.invoice_numbers.join(', ') : '') + '\\nNext: ' + next,\n      log: { flow: 'reply', customer: from, customer_email: from.toLowerCase(), invoice_numbers: c.invoice_numbers.join(', '), action: 'classified', label: c.label, promise_date: c.promise_date || '', detail: c.summary },\n    },\n    pairedItem: { item: i },\n  });\n}\nreturn out;\n"
      },
      "id": "ae72b98a-2b81-4330-b5c6-cbed2518e897",
      "name": "Read classification",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        420
      ]
    }
  ],
  "connections": {
    "Weekdays 8am": {
      "main": [
        [
          {
            "node": "Get open invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get open invoices": {
      "main": [
        [
          {
            "node": "Read cadence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read cadence": {
      "main": [
        [
          {
            "node": "Read AR log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read AR log": {
      "main": [
        [
          {
            "node": "Plan reminders",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plan reminders": {
      "main": [
        [
          {
            "node": "Email or owner task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email or owner task": {
      "main": [
        [
          {
            "node": "Claude: draft reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post to AR channel",
            "type": "main",
            "index": 0
          },
          {
            "node": "Append to AR log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude: draft reminder": {
      "main": [
        [
          {
            "node": "Check draft",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check draft": {
      "main": [
        [
          {
            "node": "Create Gmail draft",
            "type": "main",
            "index": 0
          },
          {
            "node": "Append to AR log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AR mailbox reply": {
      "main": [
        [
          {
            "node": "Claude: classify reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude: classify reply": {
      "main": [
        [
          {
            "node": "Read classification",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Read classification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read classification": {
      "main": [
        [
          {
            "node": "Post to AR channel",
            "type": "main",
            "index": 0
          },
          {
            "node": "Append to AR log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
