Files
n8n-masttro/workflows/eoMLOlRefzddaMQs_MoneyBot_read_only.json

9138 lines
400 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"updatedAt": "2026-07-01T13:25:53.463Z",
"createdAt": "2026-05-08T17:09:50.845Z",
"id": "eoMLOlRefzddaMQs",
"name": "MoneyBot-read only",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"content": "## Internal link using basic auth\n\nhttps://Masttr0!:Masttr0!@moneybot.masttro.com/webhook/672bf651-91e6-41de-b897-13e54c0e1f10/chat\n\nhttps://moneybot.masttro.com/webhook/moneybot-readonly/chat",
"height": 240,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
16000,
3984
],
"id": "d03c3c99-ddd5-4d62-a364-1db45756e92b",
"name": "Sticky Note"
},
{
"parameters": {
"content": "# 🤖 Core — Trigger · Agent · LLMs · Memory\n\nEntry point and the AI brain. Connect your preferred LLM (Sonnet, Haiku, or Ollama) to the agent. Simple Memory provides per-session context.",
"height": 800,
"width": 1868,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15584,
4288
],
"id": "830b99a8-bf8c-4944-aaa8-ee857f93a6be",
"name": "Sticky Core"
},
{
"parameters": {
"options": {
"systemMessage": "=Current datetime is {{ $now }}\n\nYou are MoneyBot, a finance automation assistant for Masttro.\n\n## ENTITIES\nMUS (US, realm 9341452327761715, USD) · MMX (Mexico, realm 9341452328017372, USD) · MSAG (Switzerland, realm 9341452373926753, CHF) · MHAG (Switzerland holding, realm 9341452561201469, CHF — JE only) · MGC (Global, realm 9130354300064436, USD — JE only)\n\n## TOOL SELECTION\n| Intent | Tool |\n|---|---|\n| Open/unpaid invoices | Postgres: view_invoices_open |\n| All invoices by date | ENTITY - Query QBO (QBQL) |\n| Payments received | Postgres: view_payments |\n| GL / revenue / expenses / P&L | Postgres: AI_view_general_ledger |\n| Account ID lookup | Postgres: chartofaccounts — always before JE |\n| HubSpot closed-won deals | Postgres: view_hubspot_closedwon |\n| HubSpot owner lookup | Postgres: hubspot_owners |\n| HubSpot all deals/companies | Fallback if not in view_hubspot_closedwon |\n| Billing schedule | Postgres: view_hubspot_closedwon |\n| Next invoice number | ENTITY - Get latest invoice DocNumber |\n| Find customer | ENTITY - Find customer by name |\n| Find item/product | ENTITY - Find item by name |\n| Ad-hoc QBO query | ENTITY - Query QBO (QBQL) |\n| Customer record by ID | ENTITY - Get a customer |\n| Create / delete / reclass JE | ENTITY - Create/delete Journal Entry |\n| Export to CSV/Excel | Export_File |\n| Contract terms / renewal dates | pgvector store first, fallback Contract_Direct_Lookup |\n| DSO / days to pay | view_payments JOIN view_invoices_open |\n| Account balances / BS / assets / liabilities / equity | Postgres: view_balancesheet |\n| Find tickets | Freshdesk - finance tickets |\n| Update ticket status / priority | Freshdesk - Update Ticket |\n| Add note / internal comment | Freshdesk - Add Note to Ticket |\n| Create new ticket | Freshdesk - Create Ticket |\n| Download invoice as PDF | Download Invoice PDF |\n| Apply credit memo to invoice | ENTITY - Apply Credit Memo to Invoice |\n\nNever dump all invoices, customers, or payments from QBO. Always use Postgres views for list queries.\n\n## POSTGRES RULES\n- AI_view_general_ledger: \"Date\" is a native DATE — always use DATE_TRUNC or CURRENT_DATE, never text strings\n- ALL mixed-case columns must be double-quoted: \"Date\", \"Account\", \"AccountType\", \"AccountSubType\", \"Classification\", \"Transaction_Type\", \"Memo_Description\", \"CustomerName\", \"Customer_Id\", \"Vendor\", \"Vendor_Id\", \"Account_Id\", \"Class\", \"Department\"\n- Lowercase (no quotes): company_id, realm_id, amount, debit, credit, exch_rate, currency\n- Never SELECT * without a WHERE clause — always use ILIKE for case-insensitive searches\n\n## QBQL RULES\nQBQL is NOT SQL. Forbidden: IN(...), OR, LIKE, ILIKE, nested parentheses, subqueries, JOINs, GROUP BY, aggregates.\nUse ORDERBY (one word), MAXRESULTS (not LIMIT, cap 100), single quotes only.\nDate filters: TxnDate >= 'YYYY-MM-DD' is valid. Derive dates from $now — never hardcode year literals.\nMultiple values: run separate calls, never chain OR.\n\nFor all-invoices-by-date, run one call per AR entity (MUS, MMX, MSAG):\n SELECT Id, DocNumber, TxnDate, CustomerRef, TotalAmt, Balance FROM Invoice\n WHERE TxnDate >= '{{ $now.toFormat(\"yyyy-MM-01\") }}' MAXRESULTS 100\n\n## QBO REFERENCE ID RULES — APPLIES TO ALL QBQL QUERIES AND WRITE OPERATIONS\nNever use a customer or item NAME as a filter value for ID-typed fields in QBQL.\nCustomerRef, ItemRef, VendorRef are reference fields — they require numeric IDs, not names.\n\nWRONG: SELECT * FROM Invoice WHERE CustomerRef = 'Ritholtz Wealth Management LLC'\nRIGHT: \n 1. SELECT Id FROM Customer WHERE DisplayName = 'Ritholtz Wealth Management LLC'\n 2. SELECT * FROM Invoice WHERE CustomerRef = '<numeric Id from step 1>'\n\nThis applies to QBQL reads as much as write payloads — resolve the Id first, always.\n\n## INVOICE CREATION WORKFLOW\n1. Find customer → get customerId + BillAddr.CountrySubDivisionCode\n2. Find item → get itemId\n3. Get latest DocNumber → +1 → collision-check CreditMemo + Estimate → use first clear number\n4. Tax code:\n - MUS: state TX or NY → 'TAX'; all others → 'NON'; unknown → 'NON'. Never omit.\n - MMX: always 'NON'\n - MSAG: query TaxCode table for Swiss VAT id\n5. Confirm details with user before creating\n\n## INVOICE SEND / UPDATE RULES\nSend: always use a literal email address — look up PrimaryEmailAddr if user says \"use the one on file\".\nUpdate (sparse): fetch SyncToken → confirm changes → copy lines verbatim from fetched array, substitute only what changed → call Update.\n\n## JOURNAL ENTRY RULES\n- Debits must equal credits — verify before posting\n- Always look up account IDs via chartofaccounts first; always populate Department\n- To modify a JE: delete existing → create corrected. Confirm debit/credit table before posting.\n- operation=delete is a URL parameter — never include in JSON body\n- MHAG/MGC: JE and Query QBO only — no invoice or customer tools\n- Entity currencies: MUS→USD · MMX→USD · MSAG→CHF · MHAG→CHF · MGC→USD\n- DocNumber format: JE-YYYYMM-NNN (max 13 chars). Reclass suffix: append \"-R\" (must stay ≤21 chars total)\n\n## RECLASS JOURNAL ENTRY\n1. Fetch Id + SyncToken via QBQL\n2. Look up accounts via chartofaccounts\n3. Confirm debit/credit table with user\n4. Delete original\n5. Post corrected (DocNumber = original + \"-R\")\n6. Confirm new JE Id\n\n## CURRENCY\nHome currencies: MUS/MMX/MGC=USD · MSAG/MHAG=CHF\nAfter any GL query with MSAG/MHAG rows: call Get FX Rate (fromCurrency=CHF), multiply by rates.USD, report USD only with note \"converted from CHF at X CHF/USD\".\nFor cross-currency deal totals: use amount_in_home_currency.\n\n## CONTRACT RETRIEVAL\n1. pgvector first — use specific clause language, one query per term\n2. If pgvector returns wrong client → Contract_Direct_Lookup\n3. If not found after both: \"This detail was not found in the retrieved contract sections.\"\n\n## BILLING SCHEDULE\nQuery view_hubspot_closedwon. Verify via pgvector if data looks wrong.\nBilling rules: Monthly→one row/month · Quarterly→every 3 months · Semi-annual→every 6 months · Annual→anniversary · NULL term→check pgvector, do not default to Annual.\n\n## LARGE DATASET HANDLING\nPhase 1 — collect silently: all filtering in SQL, LIMIT 50 OFFSET N until 0 rows.\nPhase 2 — present once: markdown tables, max 25 rows. Rows >25: show first 25 + offer export. Rows >100: skip table, call Export_File immediately.\n\n## FILE EXPORT\nFilename: report_type_YYYY-MM-DD (snake_case, derive from $now). Pass filename (no extension) + sql.\nReturn the message field verbatim. Never prefix contracts with schema.\n\n## FRESHDESK\nSearch by keyword: pass raw keyword only — e.g. acacia\nSearch by person: Contact Lookup → get email → requester:'email@domain.com' AND status:2 OR requester:'email@domain.com' AND status:3\nSingle quotes around all string values. Never use parentheses in query strings.\nStatus codes: 2=Open · 3=Pending · 4=Resolved · 5=Closed\nNotes default to private — ask if customer-visible. Always retrieve ticket Id before writing. Post in HTML format.\nConfirm before closing or resolving.\n\n## MICROSOFT TEAMS\nSend only when explicitly asked. Never auto-send after a query.\nIdentity: userId 94387110-efa9-4e7f-8f18-3332df67c0a9 · email mnolan@masttro.com\n1:1 DM: Get Teams DM Chats → find non-self member → Send Teams DM (once)\nGroup: Get Teams Group Chats → match topic/member → Send Teams DM (once)\nChannel: Get Teams List → teamId → Get Teams Channels → channelId → Send Teams Channel Message (once)\nNever guess a chatId. Any send response = done, never retry. Plain text only, max 10 rows.\n\n## OUTPUT FORMAT\n- Natural language and markdown only — no XML, tool syntax, or parameter tags\n- Never surface tool names or selection reasoning\n- Never hardcode year literals in SQL — always derive from NOW()\n- After a successful write action: clean confirmation summary only, no intermediate narration",
"maxIterations": 100,
"passthroughBinaryImages": true,
"batching": {
"batchSize": 1,
"delayBetweenBatches": 2000
}
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
16400,
4448
],
"id": "614a4991-0062-4cbe-bf33-25e8ab667b10",
"name": "AI Agent"
},
{
"parameters": {
"content": "# 🇲🇽 MMX — QuickBooks Mexico (MXN)\nRealm: 9341452328017372\n\nRead: Find customer/item, get DocNumber, ad-hoc QBQL\nWrite: Create/update customer · Create/send/update invoice\n\n> Default currency is MXN. Pass currency override if needed.",
"height": 580,
"width": 1636,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
6080
],
"id": "e6717b22-9345-41a2-81e4-05eaae719e9e",
"name": "Sticky MMX"
},
{
"parameters": {
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const c = $fromAI('currency', 'ISO currency code. MMX default is USD. Pass empty string to accept entity default.', 'string');\n const taxCodeMap = { 'TAX': 'TAX', 'NON': 'NON', '1': 'TAX', '2': 'NON' };\n\n const resolveTaxCode = (id) => taxCodeMap[String(id)] || 'NON';\n\n const p = {\n DocNumber: $fromAI('docNumber', 'The next sequential invoice number as a string', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'The numeric QuickBooks customer ID', 'string') },\n Line: JSON.parse($fromAI('lines', 'JSON array string of line items.', 'string')).map(l => ({\n Amount: l.amount ?? (l.qty * l.unitPrice),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty,\n UnitPrice: l.unitPrice,\n TaxCodeRef: { value: resolveTaxCode(l.taxCodeId) }\n }\n }))\n };\n if (c && c.trim()) p.CurrencyRef = { value: c.trim().toUpperCase() };\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
6512
],
"id": "482352cf-27e8-47a7-8194-02294478dfa3",
"name": "MMX - Create Invoice via QuickBooks API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Send a posted MMX invoice by email (realm 9341452328017372). ALWAYS have a literal email address before calling. Pass invoiceId (QBO numeric Id) and sendTo email address.",
"method": "POST",
"url": "=https://quickbooks.api.intuit.com/v3/company/9341452328017372/invoice/{{ $fromAI('invoiceId', 'The QBO numeric Invoice Id (not DocNumber)', 'string') }}/send?sendTo={{ $fromAI('sendTo', 'Recipient email address — must be a real address, never empty', 'string') }}&minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/octet-stream"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
6512
],
"id": "52a8cffc-6914-44b2-8d49-44d163df253a",
"name": "MMX - Send Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Update a posted MMX invoice (realm 9341452328017372). Requires invoiceId and syncToken from a prior QBQL query. Sparse update — only fields passed are changed.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('invoiceId', 'QBO numeric Invoice Id from prior query', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from prior query', 'string'), sparse: true }; const txnDate = $fromAI('txnDate', 'Optional new TxnDate YYYY-MM-DD, empty to skip', 'string'); const dueDate = $fromAI('dueDate', 'Optional new DueDate YYYY-MM-DD, empty to skip', 'string'); const privateNote = $fromAI('privateNote', 'Optional memo, empty to skip', 'string'); const linesJson = $fromAI('lines', 'Optional JSON array of replacement line items, empty string to skip', 'string'); if (txnDate && txnDate.trim()) p.TxnDate = txnDate.trim(); if (dueDate && dueDate.trim()) p.DueDate = dueDate.trim(); if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim(); if (linesJson && linesJson.trim()) p.Line = JSON.parse(linesJson).map(l => ({ Amount: l.amount, DetailType: 'SalesItemLineDetail', Description: l.description, SalesItemLineDetail: { ItemRef: { value: String(l.itemId) }, Qty: l.qty, UnitPrice: l.unitPrice, TaxCodeRef: { value: String(l.taxCodeId) } } })); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
6512
],
"id": "8d926a05-86f0-40b6-96c1-e14fd1484394",
"name": "MMX - Update Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MMX for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim().slice(0, 21);\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
6512
],
"id": "8b351de9-5f60-43d7-b95c-3d9ff4a6c51c",
"name": "MMX - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MMX. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16864,
6400
],
"id": "976533e1-d0d6-477f-b8b8-81567759e930",
"name": "MMX - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"content": "# 🇨🇭 MSAG — QuickBooks Switzerland (CHF)\nRealm: 9341452373926753\n\nRead: Find customer/item, get DocNumber, ad-hoc QBQL\nWrite: Create/update customer · Create/send/update invoice\n\n> Swiss VAT: 8.1% standard · 2.6% reduced · 3.8% lodging · 0% export\n> Always convert CHF→USD via Get FX Rate before reporting.",
"height": 580,
"width": 1620,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
6720
],
"id": "07586a4c-d4ef-4ec6-af83-241ae97685c2",
"name": "Sticky MSAG"
},
{
"parameters": {
"toolDescription": "Find MSAG customer(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, DisplayName, CompanyName, PrimaryEmailAddr, Active FROM Customer WHERE DisplayName LIKE '%{{ $fromAI('name_fragment', 'Substring of the customer name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
6832
],
"id": "3a01b6d9-217f-451c-bb4d-0748c3a4fba7",
"name": "MSAG - Find customer by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Find MSAG item(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, Name, Description, UnitPrice, Type, Active FROM Item WHERE Name LIKE '%{{ $fromAI('name_fragment', 'Substring of the item name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
6832
],
"id": "ab6b639a-3991-460b-a5da-e3f5c3a1968e",
"name": "MSAG - Find item by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Returns the single most recently created Invoice in MSAG. Use to determine next sequential invoice number.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "SELECT DocNumber, TxnDate, MetaData FROM Invoice ORDERBY MetaData.CreateTime DESC MAXRESULTS 1"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
6832
],
"id": "c9dea8ca-b9bf-4dd7-a7e5-09e2ac05c5cd",
"name": "MSAG - Get latest invoice DocNumber",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MSAG QuickBooks (realm 9341452373926753). ALWAYS include WHERE and MAXRESULTS. Use SELECT Id, Name, Description, Active FROM TaxCode WHERE Active = true MAXRESULTS 50 to discover Swiss VAT codes.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
6832
],
"id": "7fdec87b-3bbd-4a33-8dc9-b4de53054520",
"name": "MSAG - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"customerId": "={{ $fromAI('Customer_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15680,
6992
],
"id": "05ff2df0-96b4-482c-9120-0c81af2e1cde",
"name": "MSAG - Get a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"resource": "item",
"itemId": "={{ $fromAI('Item_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15936,
6992
],
"id": "dcd00d78-c71e-479a-b1d3-61230b07cf01",
"name": "MSAG - Get an item in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({\n DocNumber: $fromAI('docNumber', 'The next sequential invoice number as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Invoice date in YYYY-MM-DD format', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'The numeric QuickBooks customer ID', 'string') },\n Line: (() => {\n const raw = $fromAI('lines', 'JSON array of line items. Each object must have: amount (number), itemId (string), description (string), qty (number), unitPrice (number), taxCodeId (string — use the QBO TaxCode Id from the TaxCode query, never null)', 'string');\n const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\n return parsed.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: (l.taxCodeId && String(l.taxCodeId).trim()) ? String(l.taxCodeId).trim() : 'NON' }\n }\n }));\n })()\n}) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
7152
],
"id": "eed664c9-00ff-466c-ba83-4e9dfa929552",
"name": "MSAG - Create Invoice via QuickBooks API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Send a posted MSAG invoice by email (realm 9341452373926753). ALWAYS have a literal email address before calling — look it up from the customer record if needed. Pass invoiceId (QBO numeric Id, not DocNumber) and sendTo email address.",
"method": "POST",
"url": "=https://quickbooks.api.intuit.com/v3/company/9341452373926753/invoice/{{ $fromAI('invoiceId', 'The QBO numeric Invoice Id (not DocNumber) from a prior query', 'string') }}/send?sendTo={{ $fromAI('sendTo', 'Recipient email address — must be a real address, never empty', 'string') }}&minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/octet-stream"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
7152
],
"id": "47d35382-faf3-4b3d-a29c-a057e98f59cd",
"name": "MSAG - Send Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Update a posted MSAG invoice (realm 9341452373926753). Requires invoiceId and syncToken from a prior QBQL query. Sparse update — only fields passed are changed.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('invoiceId', 'QBO numeric Invoice Id from prior query', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from prior query', 'string'), sparse: true }; const txnDate = $fromAI('txnDate', 'Optional new TxnDate YYYY-MM-DD, empty to skip', 'string'); const dueDate = $fromAI('dueDate', 'Optional new DueDate YYYY-MM-DD, empty to skip', 'string'); const privateNote = $fromAI('privateNote', 'Optional memo, empty to skip', 'string'); const linesJson = $fromAI('lines', 'Optional JSON array of replacement line items, empty string to skip', 'string'); if (txnDate && txnDate.trim()) p.TxnDate = txnDate.trim(); if (dueDate && dueDate.trim()) p.DueDate = dueDate.trim(); if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim(); if (linesJson && linesJson.trim()) p.Line = JSON.parse(linesJson).map(l => ({ Amount: l.amount, DetailType: 'SalesItemLineDetail', Description: l.description, SalesItemLineDetail: { ItemRef: { value: String(l.itemId) }, Qty: l.qty, UnitPrice: l.unitPrice, TaxCodeRef: { value: String(l.taxCodeId) } } })); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
7152
],
"id": "1fddb639-ac49-48c0-af37-988dd09a1ef7",
"name": "MSAG - Update Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MSAG for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim().slice(0, 21);\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
7152
],
"id": "e07b778b-2fab-48e0-9660-f022df3887d0",
"name": "MSAG - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MSAG. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16624,
7152
],
"id": "595d91fb-ef4a-4d06-891d-3e67306659d3",
"name": "MSAG - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"content": "# 🗄 Postgres — Data Warehouse (preferred read path)\nCredential: Postgres n8n-quickbooks\n\nviews: open invoices · payments · general ledger · hubspot_deals\n\n> Always use these views for list queries — never dump from QBO directly.\n> Quote mixed-case column names.\n> Revenue in GL is negative — use ABS(SUM(amount)).",
"height": 260,
"width": 1588
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
7360
],
"id": "f6a2fe13-fafb-443a-9512-ce4907bdd22d",
"name": "Sticky Postgres"
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the view_invoices_open Postgres view. Returns UNPAID AR invoices only (balance > 0) — paid invoices excluded. For all invoices by date use ENTITY - Query QBO (QBQL) instead.\n\nAll columns lowercase, no quoting needed.\nColumns: invoice_key, company_id, docnumber, txndate, customerref_name, duedate, totalamt, balance, currencyref, exch_rate\n\nCRITICAL column types:\n txndate — native DATE. Never compare to text string.\n RIGHT: txndate = CURRENT_DATE\n RIGHT: txndate >= CURRENT_DATE - INTERVAL '7 days'\n WRONG: txndate = '2026-05-22' ← TYPE ERROR\n duedate — native DATE. Same rules.\n RIGHT: duedate < CURRENT_DATE\n WRONG: duedate < '2026-05-22' ← TYPE ERROR\n totalamt, balance — numeric\n All others — text\n\nExamples:\n -- Overdue 30+ days\n SELECT company_id, customerref_name, docnumber, duedate, balance, currencyref\n FROM view_invoices_open\n WHERE company_id IN ('MUS','MMX','MSAG')\n AND duedate <= CURRENT_DATE - INTERVAL '30 days'\n ORDER BY duedate ASC LIMIT 100\n\n -- Due in next 30 days\n SELECT company_id, customerref_name, docnumber, duedate, balance\n FROM view_invoices_open\n WHERE company_id IN ('MUS','MMX','MSAG')\n AND duedate >= CURRENT_DATE AND duedate <= CURRENT_DATE + INTERVAL '30 days'\n ORDER BY duedate ASC LIMIT 100\n\n -- AR aging summary\n SELECT company_id, customerref_name, SUM(balance) AS total_balance\n FROM view_invoices_open\n WHERE company_id IN ('MUS','MMX','MSAG')\n GROUP BY company_id, customerref_name\n ORDER BY total_balance DESC LIMIT 20",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against view_invoices_open. MUST include WHERE and LIMIT (<=500). txndate and duedate are native DATE — use CURRENT_DATE arithmetic, never quoted strings.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16224,
7456
],
"id": "f0df476e-8098-4f20-8a0b-5ed08eb2dc14",
"name": "Postgres Quickbooks open invoices",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the view_payments Postgres view for QuickBooks payments across all entities.\n\nCRITICAL: ALL columns except company_id are mixed-case — MUST be double-quoted.\n\nColumn types:\n \"TxnDate\" — native DATE. Never compare to text string.\n RIGHT: \"TxnDate\" >= CURRENT_DATE - INTERVAL '7 days'\n WRONG: \"TxnDate\" >= '2026-05-13' ← TYPE ERROR\n \"TotalAmt\" — numeric\n \"IsCustomerPayment\" — boolean\n All others — text\n\nColumns: company_id, \"TxnDate\", \"CustomerRef_Name\", \"TotalAmt\", \"CurrencyRef\", \"IsCustomerPayment\", \"PaymentMethodDerived\", \"PaymentRefNum\"\n\nExample:\n SELECT company_id, \"TxnDate\", \"CustomerRef_Name\", \"TotalAmt\", \"CurrencyRef\"\n FROM view_payments\n WHERE \"IsCustomerPayment\" = true\n AND \"TxnDate\" >= CURRENT_DATE - INTERVAL '7 days'\n AND company_id IN ('MUS','MMX','MSAG')\n ORDER BY \"TxnDate\" DESC LIMIT 100\n\nDSO: both \"TxnDate\" and txndate (from view_invoices_open) are native DATE — subtraction works directly, no cast needed.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against view_payments. MUST include WHERE and LIMIT (<=500). All mixed-case columns must be double-quoted. \"TxnDate\" is native DATE — use CURRENT_DATE arithmetic, never quoted strings.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16384,
7456
],
"id": "a00978e0-c87f-4a9b-b1ab-b206523a0a15",
"name": "Postgres Quickbooks customer payments",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the AI_view_general_ledger Postgres view for GL, P&L, revenue, expenses.\n\"Date\" is native DATE — use DATE_TRUNC or CURRENT_DATE arithmetic only, never quoted strings.\nRevenue posts negative — use ABS(SUM(amount)).\nFor expenses: SUM(amount) for net (handles reversals). Never SUM(\"Debit\") alone.\nMust include WHERE and LIMIT (<=500). Double-quote all mixed-case columns.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against \"AI_view_general_ledger\". Must include WHERE and LIMIT (<=500). \"Date\" is native DATE — use DATE_TRUNC or CURRENT_DATE arithmetic, NEVER quoted string literals like \\'2026-01-01\\'. Double-quote all mixed-case column names.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16656,
7456
],
"id": "cc2e4931-5daf-4c5a-b959-8752835d444f",
"name": "Postgres Quickbooks general ledger",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
},
"disabled": true
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query HubSpot deals and closed-won data from Postgres. Use view_hubspot_closedwon for closed-won deals and billing schedules. Use hubspot_deals for pipeline/win-loss analysis.\n\n### view_hubspot_closedwon\nAll lowercase. Already filtered to is_closed_won = true — do NOT add again.\ncontract_start_date / contract_end_date are native DATE — no cast needed.\nALWAYS include deal_url when presenting results.\n\nColumns: deal_id, deal_name, amount, amount_in_home_currency, deal_currency_code,\n close_date, contract_start_date, contract_end_date, contract_term,\n primary_associated_company, company_name, icon_alias, icon_id, icon_client_name,\n client_tier, health_status, renewal_date, churn_notice_date, churn_effective_date,\n onboarding_owner, client_success_manager, masttro_invoicing_region,\n new_pricing_model, deal_url, owner_id\n\n### hubspot_deals\nAll lowercase. Key columns: deal_id, deal_name, amount, amount_in_home_currency,\n close_date, deal_stage, pipeline, owner_id, is_closed_won, is_closed_lost,\n deal_type, contract_start_date, contract_end_date, contract_term,\n churn_date, churn_notice_date, primary_associated_company\nNote: no deal_url or deal_currency_code here — use view_hubspot_closedwon for those.\nColumn is deal_name — never dealname.\namount_in_home_currency is USD — use for cross-currency aggregations.\n\n### hubspot_owners\nColumns: owner_id, firstname, lastname, email\nJOIN: LEFT JOIN hubspot_owners o ON d.owner_id = o.owner_id\n\nBilling schedule standard query:\n SELECT deal_name, primary_associated_company, contract_term,\n contract_start_date, contract_end_date,\n amount, amount_in_home_currency, deal_currency_code, deal_id, deal_url\n FROM view_hubspot_closedwon\n WHERE contract_start_date <= (DATE_TRUNC('year', NOW()) + INTERVAL '1 year - 1 day')::date\n AND (contract_end_date >= DATE_TRUNC('year', NOW())::date OR contract_end_date IS NULL)\n ORDER BY contract_start_date ASC",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against view_hubspot_closedwon, hubspot_deals, or hubspot_owners. MUST include WHERE and LIMIT (<=500).', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
17152,
4912
],
"id": "35c66d65-cc08-4358-946e-5762e372f41c",
"name": "Hubspot deals",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"content": "# 🏦 MHAG & MGC — Journal Entry Only\nMHAG realm: 9341452561201469 (CHF)\nMGC realm: 9130354300064436 (USD)\n\nThese entities have **no invoice or customer tools**.\nAvailable: Create Journal Entry · delete Journal Entry · Query QBO (QBQL)\n\n> Use Query QBO to find Account IDs before posting.\n> MHAG amounts are CHF — convert via Get FX Rate before reporting.",
"height": 260,
"width": 1684,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
7712
],
"id": "d59faace-9090-4754-bdb6-8959ae321347",
"name": "Sticky MHAG MGC"
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MHAG for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452561201469/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (privateNote && privateNote.trim()) payload.PrivateNote = privateNote.trim();\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16736,
7792
],
"id": "3b159143-6489-4afa-923d-25784b80d894",
"name": "MHAG - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "vE8iWZDtmiPKgGao",
"name": "QuickBooks Online MHAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MHAG QuickBooks (realm 9341452561201469). ALWAYS include WHERE and MAXRESULTS.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452561201469/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16176,
7792
],
"id": "82a645c5-0635-4810-9912-0831f6dfc6f6",
"name": "MHAG - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "vE8iWZDtmiPKgGao",
"name": "QuickBooks Online MHAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MHAG. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452561201469/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16928,
7792
],
"id": "8a310b53-142a-4d42-867c-d692fd8edd4d",
"name": "MHAG - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "vE8iWZDtmiPKgGao",
"name": "QuickBooks Online MHAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MGC for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9130354300064436/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (privateNote && privateNote.trim()) payload.PrivateNote = privateNote.trim();\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16368,
7792
],
"id": "c8e3f85b-6de4-4cee-bf17-e6273d4a83e1",
"name": "MGC - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "bm9Oc2QzBM8bx9YW",
"name": "QuickBooks Online MGC"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MGC QuickBooks (realm 9130354300064436). ALWAYS include WHERE and MAXRESULTS.",
"url": "https://quickbooks.api.intuit.com/v3/company/9130354300064436/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16560,
7792
],
"id": "930fb4f2-58f1-4494-bcd7-175802124a82",
"name": "MGC - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "bm9Oc2QzBM8bx9YW",
"name": "QuickBooks Online MGC"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MGC. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9130354300064436/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17120,
7792
],
"id": "4036d440-4e01-4ec7-bf11-d4ff0b157343",
"name": "MGC - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "bm9Oc2QzBM8bx9YW",
"name": "QuickBooks Online MGC"
}
},
"disabled": true
},
{
"parameters": {
"mode": "retrieve-as-tool",
"toolDescription": "Contract terms, payment schedules, billing frequency, SLA, termination clauses, effective dates — Postgres PGVector contract store.\n\nCRITICAL: Always use specific clause language in queries, NOT generic client+contract phrases.\nGOOD queries: \"semi-annual invoice payment schedule\", \"annual license fee payment timing\", \"termination notice period\"\nBAD queries: \"49 management contract terms\", \"client contract\"\n\nUse this tool first for any contract lookup. If results return chunks from the wrong client, immediately fall back to Contract_Direct_Lookup.",
"tableName": "contracts",
"topK": 10,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.vectorStorePGVector",
"typeVersion": 1.3,
"position": [
17184,
4704
],
"id": "2f902fe9-23ea-4ebb-b953-c22f98cb9ee1",
"name": "Postgres PGVector Store",
"credentials": {
"postgres": {
"id": "YrHntSw7KF7Za0tK",
"name": "Postgres vectors"
}
}
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.embeddingsMistralCloud",
"typeVersion": 1,
"position": [
17296,
4912
],
"id": "545fd8be-c97c-41f2-9d72-963bbbc15c74",
"name": "mistral-embed",
"credentials": {
"mistralCloudApi": {
"id": "lHc1GsczHEq7NL55",
"name": "Mistral - contracts vector"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Direct SQL fallback for contract lookups. Use when pgvector returns chunks from the wrong client.\n\nTable: contracts (never vectors.contracts or public.contracts)\nColumns: id, text, metadata, client_name, contract_type, effective_date, entity, currency, section_heading, page_number, filename\nUse page_number = 1 to get one row per contract.\n\nExample:\n SELECT text, section_heading, page_number FROM contracts\n WHERE client_name ILIKE '%49 Management%'\n ORDER BY page_number ASC, id ASC LIMIT 50\n\nFor contract list:\n SELECT client_name, contract_type, entity, currency, effective_date\n FROM contracts WHERE page_number = 1\n ORDER BY client_name ASC LIMIT 200",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against the contracts table. Always filter by client_name using ILIKE. Always include ORDER BY page_number ASC, id ASC and a LIMIT.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16992,
4912
],
"id": "710e5a0d-2688-4335-80a8-74bf56246f7a",
"name": "Contract_Direct_Lookup",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Look up QuickBooks account IDs from the chartofaccounts Postgres table. ALWAYS use this before creating any journal entry — never use QBQL Account queries for ID lookups.\n\nKey columns: company_id, account_id (text — pass as-is in AccountRef), \"Name\", \"AcctNum\", \"Active\", \"Classification\", \"AccountType\"\n\nRules:\n- account_id is text — never cast it\n- Always filter by company_id\n- For multi-entity JEs: run one query per entity or use company_id IN (...)\n\nLookup by AcctNum:\n SELECT company_id, account_id, \"Name\", \"AcctNum\", \"Classification\"\n FROM chartofaccounts\n WHERE company_id = 'MUS' AND \"Active\" = true AND \"AcctNum\" ILIKE '%41440%'\n LIMIT 10\n\nLookup by name:\n SELECT company_id, account_id, \"Name\", \"AcctNum\", \"Classification\", \"AccountType\"\n FROM chartofaccounts\n WHERE company_id = 'MUS' AND \"Active\" = true AND \"Name\" ILIKE '%Bonus Expense%'\n LIMIT 10\n\nLookup multiple AcctNums at once:\n SELECT company_id, account_id, \"Name\", \"AcctNum\"\n FROM chartofaccounts\n WHERE company_id = 'MUS' AND \"Active\" = true AND (\"AcctNum\" ILIKE '%41440%' OR \"AcctNum\" ILIKE '%11210%')\n LIMIT 20\n\nLookup across entities:\n SELECT company_id, account_id, \"Name\", \"AcctNum\"\n FROM chartofaccounts\n WHERE company_id IN ('MUS','MMX','MSAG') AND \"Active\" = true AND \"AcctNum\" ILIKE '%41440%'\n LIMIT 20",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against chartofaccounts. Always filter by company_id. Always include LIMIT. Double-quote mixed-case columns: \"Name\", \"AcctNum\", \"Active\", \"Classification\", \"AccountType\". account_id is lowercase and unquoted.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16912,
7456
],
"id": "2def1f16-acee-43f9-a6b3-29e86331cac7",
"name": "Quickbooks Chart of Accounts MGC MUS MMX MSAG MHAG",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
},
"disabled": true
},
{
"parameters": {
"url": "https://graph.microsoft.com/v1.0/me/chats?$filter=chatType eq 'oneOnOne'&$expand=members&$select=id,chatType&$top=50",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17584,
4768
],
"id": "df489b7b-148f-400d-a1c0-1763df7e6b7a",
"name": "Get Teams DM Chats — returns all 1:1 chats with id and members. Search members for displayName or email matching the target person. Use chatId with Send Teams DM.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"url": "https://graph.microsoft.com/v1.0/me/chats?$filter=chatType eq 'group'&$expand=members&$select=id,chatType,topic&$top=50",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17856,
4928
],
"id": "537dd0af-08ea-4de3-b457-0c1378ccab70",
"name": "Get Teams Group Chats — returns all group chats with id, topic, and members. Match by topic name or member displayName. Use chatId with Send Teams DM.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"url": "https://graph.microsoft.com/v1.0/me/joinedTeams?$select=id,displayName",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17584,
4928
],
"id": "217303ac-0fc1-44f8-b035-ed629cfc734a",
"name": "Get Teams List — returns all joined teams with id and displayName. Call this first to resolve a team name to a teamId before calling Get Teams Channels.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"url": "=https://graph.microsoft.com/v1.0/teams/{{ $fromAI('teamId', 'The team GUID from Get Teams List matching the target team displayName', 'string') }}/channels?$select=id,displayName",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17824,
4768
],
"id": "49d75f49-785a-49f1-a28a-2b623d2d1faf",
"name": "Get Teams Channels — given a teamId from Get Teams List, returns all channels with id and displayName. Use channelId and teamId together with Send Teams Channel Message.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"resource": "chatMessage",
"chatId": {
"__rl": true,
"value": "={{ $fromAI('chatId', 'The Teams chat GUID resolved from Get Teams DM Chats or Get Teams Group Chats. For 1:1 chats, find the chat where the member whose userId is NOT 94387110-efa9-4e7f-8f18-3332df67c0a9 matches the target by email or displayName. Never guess — always resolve first.', 'string') }}",
"mode": "id"
},
"message": "={{ $fromAI('message', 'Plain text message to send. No markdown tables. Use line breaks for structure. Summarize large datasets and offer export links rather than pasting rows.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.microsoftTeamsTool",
"typeVersion": 2,
"position": [
18016,
4752
],
"id": "7680071f-4d20-45b1-903c-cf4d3d56bf9d",
"name": "Send Teams DM",
"webhookId": "d0bf3351-3696-4ea0-adbd-ddec03926981",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"resource": "channelMessage",
"teamId": {
"__rl": true,
"value": "={{ $fromAI('teamId', 'The team GUID from Get Teams List matching the target team displayName', 'string') }}",
"mode": "id"
},
"channelId": {
"__rl": true,
"value": "={{ $fromAI('channelId', 'The channel GUID from Get Teams Channels matching the target channel displayName', 'string') }}",
"mode": "id"
},
"message": "={{ $fromAI('message', 'Plain text message to post. No markdown tables. Use line breaks for structure. Summarize large datasets and offer export links rather than pasting rows.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.microsoftTeamsTool",
"typeVersion": 2,
"position": [
18048,
4912
],
"id": "03b2b68c-19a6-4cbe-ac06-ee2e3749691b",
"name": "Send Teams Channel Message",
"webhookId": "fb75f216-5c71-4ea0-867b-90abb819a51c",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"operation": "create",
"displayName": "={{ $fromAI('Display_Name', `The customer's full name or company name`, 'string') }}",
"additionalFields": {
"PrimaryEmailAddr": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Primary_Email_Address', ``, 'string') }}",
"PrimaryPhone": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Primary_Phone', ``, 'string') }}"
}
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
16896,
6800
],
"id": "589485a7-2896-4635-8324-713bc9e2d93f",
"name": "MSAG - Create customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the \"view_balancesheet\" Postgres view for balance sheet data — Assets, Liabilities, and Equity as of a specific date.\n\nThis view represents CUMULATIVE balances (sum of all transactions up to a point in time), not period activity. Use this for: account balances, net assets, total liabilities, equity position, cash balances, AR/AP balances, and any question about what something IS WORTH or OWES as of a date.\n\nFull column list:\n company_id (text), realm_id (text), report_name (text),\n start_period (text), end_period (text), currency (text),\n accounting_method (text), report_run_at (timestamptz),\n section_path (text), row_group (text), row_type (text),\n row_label (text), account_id (text), depth (integer),\n col_title (text), col_start_date (text), col_end_date (text),\n col_amount (numeric), \"Account Key\" (text), account_name (text)\n\nKEY CONCEPTS:\n - col_amount is the balance AS OF col_end_date (cumulative, not period delta)\n - section_path identifies the BS section: 'Assets', 'Liabilities', 'Equity'\n - row_type = 'Data' for actual account rows; always filter row_type = 'Data' to avoid double-counting totals/headers\n - row_label = human-readable account name\n - depth = hierarchy level (lower = summary, higher = detail)\n - col_end_date is TEXT stored as YYYY-MM-DD — always cast with ::date for comparisons\n\nDATE FILTERING (col_end_date is TEXT — always cast):\n RIGHT: col_end_date::date <= CURRENT_DATE\n RIGHT: col_end_date::date = DATE_TRUNC('month', NOW())::date - INTERVAL '1 day'\n WRONG: col_end_date <= '2026-01-01' ← text comparison, wrong sort order\n\nTo get the most recent snapshot only:\n Use ORDER BY report_run_at DESC LIMIT N, or filter col_end_date::date = (SELECT MAX(col_end_date::date) FROM view_balancesheet WHERE company_id = '...')\n\nCOMMON QUERY TEMPLATES:\n\nLatest balance sheet detail by section:\n SELECT company_id, section_path, row_label, col_amount, col_end_date\n FROM view_balancesheet\n WHERE row_type = 'Data'\n AND company_id IN ('MUS','MMX','MSAG','MHAG','MGC')\n AND col_end_date::date <= CURRENT_DATE\n ORDER BY company_id, section_path, depth, row_label\n LIMIT 500\n\nTotal assets / liabilities / equity summary:\n SELECT company_id, section_path, SUM(col_amount) AS total\n FROM view_balancesheet\n WHERE row_type = 'Data'\n AND company_id IN ('MUS','MMX','MSAG','MHAG','MGC')\n AND col_end_date::date <= CURRENT_DATE\n GROUP BY company_id, section_path\n ORDER BY company_id, section_path\n LIMIT 100\n\nSpecific account balance (e.g. cash):\n SELECT company_id, row_label, col_amount, col_end_date\n FROM view_balancesheet\n WHERE row_type = 'Data'\n AND row_label ILIKE '%cash%'\n AND company_id IN ('MUS','MMX','MSAG','MHAG','MGC')\n AND col_end_date::date <= CURRENT_DATE\n ORDER BY company_id, col_end_date::date DESC\n LIMIT 100\n\nCURRENCY:\n MSAG and MHAG report in CHF. After any query returning CHF rows: call Get FX Rate (fromCurrency=CHF),\n multiply col_amount by rate, report in USD with note \"converted from CHF at X CHF/USD\".\n\nDO NOT use this view for: revenue, expenses, P&L, or period-over-period income — use AI_view_general_ledger instead.\nDO NOT SUM rows where row_type != 'Data' — header and total rows double-count.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against \"view_balancesheet\". Must include WHERE and LIMIT (<=500). col_end_date is TEXT — always cast with ::date for date comparisons. Double-quote mixed-case columns like \"Account Key\". Always filter row_type = \\'Data\\' unless intentionally pulling summary totals.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
17088,
7456
],
"id": "8177c53a-6296-4b5a-a383-bc1fc1787d59",
"name": "Postgres Quickbooks balance sheet",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a customer payment in MSAG (realm 9341452373926753, default currency CHF).\n\nWorkflow (follow in order):\n 1. Resolve customerId via MSAG - Find customer by name\n 2. Get open invoice Ids and balances via Postgres view_invoices_open (filter company_id = 'MSAG')\n 3. Validate: totalAmt MUST equal sum of all line amounts — QBO will reject mismatches\n 4. Confirm payment details with user before posting\n\nParameters:\n customerId QBO numeric customer Id\n totalAmt Total payment amount (number) — must match sum of lines\n txnDate Payment date YYYY-MM-DD\n lines JSON array: [{\"invoiceId\": \"123\", \"amount\": 500.00}]\n Pass empty array [] to post as unapplied credit\n currencyRef ISO currency code — empty string for entity default (CHF)\n paymentMethodRef Optional QBO payment method Id — empty string to omit\n depositToAccountRef Optional QBO bank account Id — empty string for Undeposited Funds\n privateNote Optional internal memo — empty string to omit\nAfter posting, call Get FX Rate (fromCurrency=CHF) to convert the amount to USD for any reporting.\n\nAfter a successful POST (Payment.Id present in response), return \nonly: Payment.Id, TotalAmt, TxnDate, CustomerRef name, and linked \ninvoice Id. Do not re-fetch the payment or invoice. Ignore any \nerror fields present in the same response body — treat them as \nnon-fatal noise and never surface raw error JSON to the user.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/payment?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const customerId = $fromAI('customerId', 'QBO numeric customer Id', 'string');\n const totalAmt = $fromAI('totalAmt', 'Total payment amount as a number. MUST equal sum of all line amounts.', 'number');\n const txnDate = $fromAI('txnDate', 'Payment date YYYY-MM-DD', 'string');\n const currencyRef = $fromAI('currencyRef', 'ISO currency code. Pass empty string for entity default.', 'string');\n const paymentMethodRef = $fromAI('paymentMethodRef', 'Optional QBO payment method Id. Empty string to omit.', 'string');\n const depositToAccountRef = $fromAI('depositToAccountRef', 'Optional QBO bank account Id to deposit to. Empty string for Undeposited Funds.', 'string');\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n const linesRaw = $fromAI('lines', 'JSON array linking payment to invoices: [{\"invoiceId\": \"123\", \"amount\": 500.00}]. Empty array [] for unapplied credit.', 'string');\n const payload = { CustomerRef: { value: String(customerId) }, TotalAmt: Number(totalAmt), TxnDate: txnDate };\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n if (lines && lines.length > 0) { payload.Line = lines.map(l => ({ Amount: Number(l.amount), LinkedTxn: [{ TxnId: String(l.invoiceId), TxnType: 'Invoice' }] })); }\n if (currencyRef && currencyRef.trim()) { payload.CurrencyRef = { value: currencyRef.trim().toUpperCase() }; }\n if (paymentMethodRef && paymentMethodRef.trim()) { payload.PaymentMethodRef = { value: paymentMethodRef.trim() }; }\n if (depositToAccountRef && depositToAccountRef.trim()) { payload.DepositToAccountRef = { value: depositToAccountRef.trim() }; }\n if (privateNote && privateNote.trim()) { payload.PrivateNote = privateNote.trim(); }\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16784,
7008
],
"id": "ec41cb54-6c99-4ddc-be76-bfb4fe61371c",
"name": "MSAG - Create Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Void a customer payment in MSAG (realm 9341452373926753). Use to reverse a payment that was posted in error.\n\nQBO does not delete payments — voiding zeroes the amount and marks it void while preserving the transaction record.\n\nSteps (all required):\n 1. Retrieve the payment Id and SyncToken via MSAG - Query QBO (QBQL):\n SELECT Id, SyncToken, TxnDate, TotalAmt FROM Payment WHERE Id = '<id>' MAXRESULTS 1\n 2. Present the payment details to user and confirm before voiding\n 3. Call this tool with Id and SyncToken\n\nThe void is irreversible. Always confirm with user first.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/payment?minorversion=65&operation=void",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('payment_id', 'The QBO numeric Payment Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken — retrieve via QBQL first', 'string'), sparse: true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16640,
6864
],
"id": "19fde7fa-7a54-49f0-bbcc-45f7b23c4d81",
"name": "MSAG - Void Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Void a customer payment in MMX (realm 9341452328017372). Use to reverse a payment that was posted in error.\n\nQBO does not delete payments — voiding zeroes the amount and marks it void while preserving the transaction record.\n\nSteps (all required):\n 1. Retrieve the payment Id and SyncToken via MMX - Query QBO (QBQL):\n SELECT Id, SyncToken, TxnDate, TotalAmt FROM Payment WHERE Id = '<id>' MAXRESULTS 1\n 2. Present the payment details to user and confirm before voiding\n 3. Call this tool with Id and SyncToken\n\nThe void is irreversible. Always confirm with user first.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/payment?minorversion=65&operation=void",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('payment_id', 'The QBO numeric Payment Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken — retrieve via QBQL first', 'string'), sparse: true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16992,
6480
],
"id": "439ff7c5-f393-4637-a3c7-1902e2d1194b",
"name": "MMX - Void Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Void a customer payment in MUS (realm 9341452327761715). Use to reverse a payment that was posted in error.\n\nQBO does not delete payments — voiding zeroes the amount and marks it void while preserving the transaction record.\n\nSteps (all required):\n 1. Retrieve the payment Id and SyncToken via MUS - Query QBO (QBQL):\n SELECT Id, SyncToken, TxnDate, TotalAmt FROM Payment WHERE Id = '<id>' MAXRESULTS 1\n 2. Present the payment details to user and confirm before voiding\n 3. Call this tool with Id and SyncToken\n\nThe void is irreversible. Always confirm with user first.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/payment?minorversion=65&operation=void",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('payment_id', 'The QBO numeric Payment Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken — retrieve via QBQL first', 'string'), sparse: true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16992,
5824
],
"id": "b7152cba-ef41-4dcb-8779-1c9993e3c2c8",
"name": "MUS - Void Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a customer payment in MMX (realm 9341452328017372, default currency USD).\n\nWorkflow (follow in order):\n 1. Resolve customerId via MMX - Find customer by name\n 2. Get open invoice Ids and balances via Postgres view_invoices_open (filter company_id = 'MMX')\n 3. Validate: totalAmt MUST equal sum of all line amounts — QBO will reject mismatches\n 4. Confirm payment details with user before posting\n\nParameters:\n customerId QBO numeric customer Id\n totalAmt Total payment amount (number) — must match sum of lines\n txnDate Payment date YYYY-MM-DD\n lines JSON array: [{\"invoiceId\": \"123\", \"amount\": 500.00}]\n Pass empty array [] to post as unapplied credit\n currencyRef ISO currency code — empty string for entity default (USD)\n paymentMethodRef Optional QBO payment method Id — empty string to omit\n depositToAccountRef Optional QBO bank account Id — empty string for Undeposited Funds\n privateNote Optional internal memo — empty string to omit\n\nAfter a successful POST (Payment.Id present in response), return \nonly: Payment.Id, TotalAmt, TxnDate, CustomerRef name, and linked \ninvoice Id. Do not re-fetch the payment or invoice. Ignore any \nerror fields present in the same response body — treat them as \nnon-fatal noise and never surface raw error JSON to the user.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/payment?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const customerId = $fromAI('customerId', 'QBO numeric customer Id', 'string');\n const totalAmt = $fromAI('totalAmt', 'Total payment amount as a number. MUST equal sum of all line amounts.', 'number');\n const txnDate = $fromAI('txnDate', 'Payment date YYYY-MM-DD', 'string');\n const currencyRef = $fromAI('currencyRef', 'ISO currency code. Pass empty string for entity default.', 'string');\n const paymentMethodRef = $fromAI('paymentMethodRef', 'Optional QBO payment method Id. Empty string to omit.', 'string');\n const depositToAccountRef = $fromAI('depositToAccountRef', 'Optional QBO bank account Id to deposit to. Empty string for Undeposited Funds.', 'string');\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n const linesRaw = $fromAI('lines', 'JSON array linking payment to invoices: [{\"invoiceId\": \"123\", \"amount\": 500.00}]. Empty array [] for unapplied credit.', 'string');\n const payload = { CustomerRef: { value: String(customerId) }, TotalAmt: Number(totalAmt), TxnDate: txnDate };\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n if (lines && lines.length > 0) { payload.Line = lines.map(l => ({ Amount: Number(l.amount), LinkedTxn: [{ TxnId: String(l.invoiceId), TxnType: 'Invoice' }] })); }\n if (currencyRef && currencyRef.trim()) { payload.CurrencyRef = { value: currencyRef.trim().toUpperCase() }; }\n if (paymentMethodRef && paymentMethodRef.trim()) { payload.PaymentMethodRef = { value: paymentMethodRef.trim() }; }\n if (depositToAccountRef && depositToAccountRef.trim()) { payload.DepositToAccountRef = { value: depositToAccountRef.trim() }; }\n if (privateNote && privateNote.trim()) { payload.PrivateNote = privateNote.trim(); }\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16704,
6480
],
"id": "001d5dd2-5824-4219-95ca-c6f1fc855044",
"name": "MMX - Create Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a customer payment in MUS (realm 9341452327761715, default currency USD).\n\nWorkflow (follow in order):\n 1. Resolve customerId via MUS - Find customer by name\n 2. Get open invoice Ids and balances via Postgres view_invoices_open (filter company_id = 'MUS')\n 3. Validate: totalAmt MUST equal sum of all line amounts — QBO will reject mismatches\n 4. Confirm payment details with user before posting\n\nParameters:\n customerId QBO numeric customer Id\n totalAmt Total payment amount (number) — must match sum of lines\n txnDate Payment date YYYY-MM-DD\n lines JSON array: [{\"invoiceId\": \"123\", \"amount\": 500.00}]\n Pass empty array [] to post as unapplied credit\n currencyRef ISO currency code — empty string for entity default (USD)\n paymentMethodRef Optional QBO payment method Id — empty string to omit\n depositToAccountRef Optional QBO bank account Id — empty string for Undeposited Funds\n privateNote Optional internal memo — empty string to omit\n\nAfter a successful POST (Payment.Id present in response), return \nonly: Payment.Id, TotalAmt, TxnDate, CustomerRef name, and linked \ninvoice Id. Do not re-fetch the payment or invoice. Ignore any \nerror fields present in the same response body — treat them as \nnon-fatal noise and never surface raw error JSON to the user.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/payment?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const customerId = $fromAI('customerId', 'QBO numeric customer Id', 'string');\n const totalAmt = $fromAI('totalAmt', 'Total payment amount as a number. MUST equal sum of all line amounts.', 'number');\n const txnDate = $fromAI('txnDate', 'Payment date YYYY-MM-DD', 'string');\n const currencyRef = $fromAI('currencyRef', 'ISO currency code. Pass empty string for entity default.', 'string');\n const paymentMethodRef = $fromAI('paymentMethodRef', 'Optional QBO payment method Id. Empty string to omit.', 'string');\n const depositToAccountRef = $fromAI('depositToAccountRef', 'Optional QBO bank account Id to deposit to. Empty string for Undeposited Funds.', 'string');\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n const linesRaw = $fromAI('lines', 'JSON array linking payment to invoices: [{\"invoiceId\": \"123\", \"amount\": 500.00}]. Empty array [] for unapplied credit.', 'string');\n const payload = { CustomerRef: { value: String(customerId) }, TotalAmt: Number(totalAmt), TxnDate: txnDate };\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n if (lines && lines.length > 0) { payload.Line = lines.map(l => ({ Amount: Number(l.amount), LinkedTxn: [{ TxnId: String(l.invoiceId), TxnType: 'Invoice' }] })); }\n if (currencyRef && currencyRef.trim()) { payload.CurrencyRef = { value: currencyRef.trim().toUpperCase() }; }\n if (paymentMethodRef && paymentMethodRef.trim()) { payload.PaymentMethodRef = { value: paymentMethodRef.trim() }; }\n if (depositToAccountRef && depositToAccountRef.trim()) { payload.DepositToAccountRef = { value: depositToAccountRef.trim() }; }\n if (privateNote && privateNote.trim()) { payload.PrivateNote = privateNote.trim(); }\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16688,
5776
],
"id": "dea99ea2-8e0c-4718-beb1-2f5eff72b58a",
"name": "MUS - Create Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"content": "# 🛠 Shared Tools — FX · EML · Collections · Risk Scoring · File Export\n\nCross-entity utilities available to the agent at all times.",
"height": 240,
"width": 1832,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
5136
],
"id": "cd2b86b1-7002-4c75-a85f-82fc0daf7ab5",
"name": "Sticky Other Tools"
},
{
"parameters": {
"customerId": "={{ $fromAI('Customer_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15680,
5712
],
"id": "c8fe2dc6-a7f3-4919-a41e-c318fef0ac51",
"name": "MUS - Get a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"resource": "item",
"itemId": "={{ $fromAI('Item_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15936,
5712
],
"id": "5ff329b4-a21a-4d36-9bc0-2f6434ca15cb",
"name": "MUS - Get an item in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"operation": "create",
"displayName": "={{ $fromAI('Display_Name', `The customer's full name or company name`, 'string') }}",
"additionalFields": {}
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
16752,
5520
],
"id": "a99476ad-e564-4cb3-bbd6-c869ca3df54b",
"name": "MUS - Create customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const TAXABLE_STATES = ['TX', 'NY'];\n const c = $fromAI('currency', 'ISO currency code. MUS default is USD. Pass empty string to accept entity default.', 'string');\n const customerState = $fromAI('customerState', 'Two-letter US state code from customer BillAddr.CountrySubDivisionCode. Pass empty string if unknown.', 'string');\n\n const stateTaxCode = TAXABLE_STATES.includes((customerState || '').toUpperCase().trim()) ? 'TAX' : 'NON';\n\n const resolveTaxCode = (id) => {\n const map = { 'TAX': 'TAX', 'NON': 'NON', '1': 'TAX', '2': 'NON' };\n return map[String(id)] || stateTaxCode;\n };\n\n const p = {\n DocNumber: $fromAI('docNumber', 'The next sequential invoice number as a string', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'The numeric QuickBooks customer ID', 'string') },\n Line: JSON.parse($fromAI('lines', 'JSON array string of line items.', 'string')).map(l => ({\n Amount: l.amount ?? (l.qty * l.unitPrice),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty,\n UnitPrice: l.unitPrice,\n TaxCodeRef: { value: resolveTaxCode(l.taxCodeId) }\n }\n }))\n };\n if (c && c.trim()) p.CurrencyRef = { value: c.trim().toUpperCase() };\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
5872
],
"id": "3677c975-990e-4f83-9392-20afb217334d",
"name": "MUS - Create Invoice via QuickBooks API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Send a posted MUS invoice by email (realm 9341452327761715). ALWAYS have a literal email address before calling — look it up from the customer record if needed. Pass invoiceId (the QBO numeric Id, not DocNumber) and sendTo email address.",
"method": "POST",
"url": "=https://quickbooks.api.intuit.com/v3/company/9341452327761715/invoice/{{ $fromAI('invoiceId', 'The QBO numeric Invoice Id (not DocNumber) from a prior query', 'string') }}/send?sendTo={{ $fromAI('sendTo', 'Recipient email address — must be a real address, never empty', 'string') }}&minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/octet-stream"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
5872
],
"id": "33c6c517-4056-4493-a1cc-ae381492feb1",
"name": "MUS - Send Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"toolDescription": "Update a posted MUS invoice (realm 9341452327761715). Requires invoiceId and syncToken from a prior QBQL query. Uses sparse update — only fields you pass are changed. Pass lines as full replacement line array if changing line items.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('invoiceId', 'QBO numeric Invoice Id from prior query', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from prior query — required for updates', 'string'), sparse: true }; const txnDate = $fromAI('txnDate', 'Optional new transaction date YYYY-MM-DD, empty string to skip', 'string'); const dueDate = $fromAI('dueDate', 'Optional new due date YYYY-MM-DD, empty string to skip', 'string'); const privateNote = $fromAI('privateNote', 'Optional memo/note, empty string to skip', 'string'); const linesJson = $fromAI('lines', 'Optional JSON array of replacement line items (same format as Create Invoice). Pass empty string to leave lines unchanged.', 'string'); if (txnDate && txnDate.trim()) p.TxnDate = txnDate.trim(); if (dueDate && dueDate.trim()) p.DueDate = dueDate.trim(); if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim(); if (linesJson && linesJson.trim()) p.Line = JSON.parse(linesJson).map(l => ({ Amount: l.amount, DetailType: 'SalesItemLineDetail', Description: l.description, SalesItemLineDetail: { ItemRef: { value: String(l.itemId) }, Qty: l.qty, UnitPrice: l.unitPrice, TaxCodeRef: { value: String(l.taxCodeId) } } })); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
5872
],
"id": "50204815-05ef-44ba-b4c8-6bd270632103",
"name": "MUS - Update Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MUS for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Each line: account_id, posting_type (Debit|Credit), amount, description, entity_type, entity_id, department_id, class_id. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim().slice(0, 21);\n if (privateNote && privateNote.trim()) payload.PrivateNote = privateNote.trim();\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
5872
],
"id": "cfb624f1-4949-41ac-8f37-e657302347b1",
"name": "MUS - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MUS. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16848,
5712
],
"id": "6be9cf50-13a3-4729-ae7c-fca8f475e339",
"name": "MUS - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Exports query results to a CSV file and returns a download URL. Pass a complete SELECT SQL query and a filename.\n\nParameters:\n- filename: snake_case name without extension, e.g. hosting_expense_2026-05-23\n- sql: complete SELECT query — no LIMIT unless intentional. Follow all column quoting rules for each view.\n\nNever assemble rows in memory for export — always pass the SQL directly.\nFor GL exports: \"Date\" is native DATE — use DATE_TRUNC or CURRENT_DATE, never quoted string literals.",
"method": "POST",
"url": "http://10.116.0.2:5679/webhook/export-insert",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "filename",
"value": "={{ $fromAI('filename', 'CSV filename without extension, snake_case, e.g. hosting_expense_2026-05-23') }}"
},
{
"name": "sql",
"value": "={{ $fromAI('sql', 'Complete SELECT SQL query to execute against the Postgres data warehouse') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
16464,
5232
],
"id": "818bb6ad-6bb5-420e-9998-39123c65a3f0",
"name": "Export_File"
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MUS QuickBooks (realm 9341452327761715). ALWAYS include WHERE and MAXRESULTS.\n\nQBQL RULES: NEVER use IN(...), OR, nested parentheses, subqueries, JOINs, GROUP BY, aggregates.\nUse ORDERBY (not ORDER BY). Use MAXRESULTS (not LIMIT). Single quotes only.\nException: TxnDate date filters with = or >= and ISO date strings are valid.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
5552
],
"id": "d1aa28ce-a737-49a1-ac0f-f769972bd48f",
"name": "MUS - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Returns the single most recently created Invoice in MUS. Use to determine next sequential invoice number.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "SELECT DocNumber, TxnDate, MetaData FROM Invoice ORDERBY MetaData.CreateTime DESC MAXRESULTS 1"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
5552
],
"id": "b4989e6a-a3d0-44dd-acdd-e8bcc1defa4b",
"name": "MUS - Get latest invoice DocNumber",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Find MUS item(s) (products/services) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, Name, Description, UnitPrice, Type, Active FROM Item WHERE Name LIKE '%{{ $fromAI('name_fragment', 'Substring of the item name to search for', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
5552
],
"id": "1c447868-154d-44d9-8b68-87c90e945f91",
"name": "MUS - Find item by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"toolDescription": "Find MUS customer(s) by name fragment using QBQL LIKE. Returns up to 20 matches with Id, DisplayName, CompanyName, PrimaryEmailAddr.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, DisplayName, CompanyName, PrimaryEmailAddr, Active FROM Customer WHERE DisplayName LIKE '%{{ $fromAI('name_fragment', 'Substring of the customer name to search for', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
5552
],
"id": "0354e1d0-97ef-4a40-9968-18da85b07a92",
"name": "MUS - Find customer by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"jsCode": "const { customer_id, entity } = $input.item.json;\n\nif (!customer_id || !entity) {\n return [{ json: { error: 'customer_id and entity are required' } }];\n}\n\nconst company_id = entity;\n\nconst arResult = await $db.executeQuery(\n `SELECT\n COALESCE(SUM(balance), 0)::NUMERIC AS open_ar,\n COALESCE(MAX(CURRENT_DATE - duedate), 0) AS max_dpd,\n COUNT(*) FILTER (WHERE duedate < CURRENT_DATE) AS overdue_count\n FROM view_invoices_open\n WHERE customerref_name ILIKE $1\n AND company_id = $2`,\n [`%${customer_id}%`, company_id]\n);\n\nconst paymentResult = await $db.executeQuery(\n `SELECT COUNT(*) AS late_payments\n FROM view_payments p\n JOIN view_invoices_open i\n ON p.company_id = i.company_id\n AND p.\"CustomerRef_Name\" = i.customerref_name\n WHERE p.\"CustomerRef_Name\" ILIKE $1\n AND p.company_id = $2\n AND p.\"IsCustomerPayment\" = true\n AND p.\"TxnDate\" >= CURRENT_DATE - INTERVAL '6 months'\n AND p.\"TxnDate\" > i.duedate`,\n [`%${customer_id}%`, company_id]\n);\n\nconst row = arResult.rows[0];\nconst openAr = parseFloat(row.open_ar) || 0;\nconst maxDpd = parseInt(row.max_dpd) || 0;\nconst overdueCount = parseInt(row.overdue_count) || 0;\nconst latePayments = parseInt(paymentResult.rows[0].late_payments) || 0;\n\nlet score = 0;\nconst flags = [];\n\nif (maxDpd > 90) { score += 40; flags.push('90+ days past due'); }\nelse if (maxDpd > 60) { score += 25; flags.push('60–90 days past due'); }\nelse if (maxDpd > 30) { score += 10; flags.push('30–60 days past due'); }\n\nif (openAr > 100000) { score += 25; flags.push(`High open AR: $${openAr.toLocaleString()}`); }\nelse if (openAr > 50000) { score += 15; flags.push(`Elevated open AR: $${openAr.toLocaleString()}`); }\nelse if (openAr > 10000) { score += 5; flags.push(`Moderate open AR: $${openAr.toLocaleString()}`); }\n\nif (overdueCount > 5) { score += 20; flags.push(`${overdueCount} overdue invoices`); }\nelse if (overdueCount > 2) { score += 10; flags.push(`${overdueCount} overdue invoices`); }\nelse if (overdueCount > 0) { score += 5; flags.push(`${overdueCount} overdue invoice`); }\n\nif (latePayments > 5) { score += 15; flags.push(`${latePayments} late payments in last 6 months`); }\nelse if (latePayments > 2) { score += 8; flags.push(`${latePayments} late payments in last 6 months`); }\nelse if (latePayments > 0) { score += 3; flags.push(`${latePayments} late payment in last 6 months`); }\n\nconst rating = score >= 60 ? 'HIGH' : score >= 30 ? 'MEDIUM' : 'LOW';\n\nconst action =\n rating === 'HIGH' ? 'Escalate to finance leadership. Use formal tone in any outreach.' :\n rating === 'MEDIUM' ? 'Send collection reminder. Flag for follow-up in 5 business days.' :\n 'Standard collections cycle. Friendly reminder tone appropriate.';\n\nreturn [{ json: {\n customer_id,\n company_id,\n score,\n rating,\n flags,\n action,\n detail: {\n open_ar: openAr,\n max_dpd: maxDpd,\n overdue_invoices: overdueCount,\n late_payments_6mo: latePayments\n }\n}}];"
},
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.3,
"position": [
16240,
5232
],
"id": "f5d62a0f-c0e3-4391-a40b-24484be8a2bf",
"name": "score_customer_risk"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "H7My4glPDt56gXJz",
"mode": "list",
"cachedResultUrl": "/workflow/H7My4glPDt56gXJz",
"cachedResultName": "Collections Emails"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
16064,
5216
],
"id": "da32d790-a179-416b-83ff-0281932b67f4",
"name": "Call 'MoneyBot — Collections Emails'",
"disabled": true
},
{
"parameters": {
"name": "Generate_EML_File",
"description": "Generates a .eml email file and saves it to /files/emails/. Use when the user asks to create an email file, export an email, generate a .eml, or draft a payment reminder or invoice email as a downloadable file. Pass: to (recipient), from (sender), subject, body (plain text), cc (optional). Returns the saved file path.",
"workflowId": {
"__rl": true,
"value": "QWWFHJ57u8UvM11y",
"mode": "list",
"cachedResultUrl": "/workflow/QWWFHJ57u8UvM11y",
"cachedResultName": "Sub-Workflow EML generator"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"to": "={{ $fromAI('to', 'Recipient email address', 'string') }}",
"from": "={{ $fromAI('from', 'Sender email address', 'string') }}",
"subject": "={{ $fromAI('subject', 'Email subject line', 'string') }}",
"body": "={{ $fromAI('body', 'Plain text email body content', 'string') }}",
"cc": "={{ $fromAI('cc', 'Optional CC email address, empty string if none', 'string') }}"
},
"matchingColumns": [],
"schema": [
{
"id": "to",
"displayName": "to",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "from",
"displayName": "from",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "subject",
"displayName": "subject",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "body",
"displayName": "body",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "cc",
"displayName": "cc",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
}
},
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2,
"position": [
15872,
5232
],
"id": "0a54f690-895c-47ef-90c3-3c328a1429d5",
"name": "Generate EML File",
"disabled": true
},
{
"parameters": {
"toolDescription": "Fetch current exchange rates using exchangerate-api.com. Pass fromCurrency (e.g. CHF, USD) and the response contains rates to all major currencies. Use to convert MSAG/MHAG CHF amounts to USD for reporting.",
"url": "=https://api.exchangerate-api.com/v4/latest/{{ $fromAI('fromCurrency', 'Source currency code (e.g. CHF, USD, EUR)', 'string') || 'USD' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
},
{
"name": "User-Agent",
"value": "n8n-workflow"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
5232
],
"id": "69a1e4e4-3b8b-46e8-b42f-39d741760c07",
"name": "Get FX Rate"
},
{
"parameters": {
"operation": "create",
"displayName": "={{ $fromAI('Display_Name', `The customer's full name or company name`, 'string') }}",
"additionalFields": {}
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
16784,
6192
],
"id": "80640cac-e62e-40e7-aaf2-94408e5010f7",
"name": "MMX - Create customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"resource": "item",
"itemId": "={{ $fromAI('Item_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15936,
6352
],
"id": "692b658b-b4b5-4fae-ac94-d1494847fa84",
"name": "MMX - Get an item in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"customerId": "={{ $fromAI('Customer_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15680,
6352
],
"id": "3363a89a-35d6-41d8-948a-b6fe9294d8d2",
"name": "MMX - Get a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MMX QuickBooks (realm 9341452328017372). ALWAYS include WHERE and MAXRESULTS.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
6192
],
"id": "ebb80d71-b6ed-430c-bea9-32d7b636e543",
"name": "MMX - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Returns the single most recently created Invoice in MMX. Use to determine next sequential invoice number.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "SELECT DocNumber, TxnDate, MetaData FROM Invoice ORDERBY MetaData.CreateTime DESC MAXRESULTS 1"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
6192
],
"id": "636e1e14-dd99-4708-8bf8-c75a6197058f",
"name": "MMX - Get latest invoice DocNumber",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Find MMX item(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, Name, Description, UnitPrice, Type, Active FROM Item WHERE Name LIKE '%{{ $fromAI('name_fragment', 'Substring of the item name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
6192
],
"id": "72157b53-e739-47ad-87b4-a41aba1c7e9e",
"name": "MMX - Find item by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Find MMX customer(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, DisplayName, CompanyName, PrimaryEmailAddr, Active FROM Customer WHERE DisplayName LIKE '%{{ $fromAI('name_fragment', 'Substring of the customer name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
6192
],
"id": "cb91a632-0272-4ac7-88d3-23a9e73159db",
"name": "MMX - Find customer by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"content": "# 🇺🇸 MUS — QuickBooks US (USD)\nRealm: 9341452327761715\n\nRead: Find customer/item, get DocNumber, ad-hoc QBQL\nWrite: Create/update customer · Create/send/update invoice\n\n> Always look up TaxCode before creating invoices.",
"height": 580,
"width": 1652,
"color": 2
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
5440
],
"id": "c1bb76f7-c40e-48bc-a442-da54d7647010",
"name": "Sticky MUS"
},
{
"parameters": {
"content": "# Freshdesk",
"height": 544,
"width": 656,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
17344,
5488
],
"id": "7cea2fb8-749f-40db-9760-5e634ac9de96",
"name": "Sticky Note1"
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/contacts/autocomplete",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "term",
"value": "={{ $fromAI('contact_name', 'The name or partial name of the contact to search for. Examples: \"elsa peretti\", \"john\", \"smith\". Do not include field prefixes.', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
17408,
5648
],
"id": "a65faea6-3bef-4603-83c0-221173d303e2",
"name": "Freshdesk Contact Lookup - Finance",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Get many tickets in Freshdesk for Finance.\nWhen looking at open tickets, use status 2 and status 3 and check all batches.",
"operation": "getAll",
"returnAll": "=",
"options": {
"include": [
"description",
"company",
"stats",
"requester"
]
}
},
"type": "n8n-nodes-base.freshdeskTool",
"typeVersion": 1,
"position": [
17520,
5808
],
"id": "b8cb4668-edd7-477c-9aa6-5b158ec16678",
"name": "Freshdesk - finance tickets",
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Get many tickets in Freshdesk for customer support team.\nWhen looking at open tickets, use status 2 and status 3 and check all batches.",
"operation": "getAll",
"returnAll": "=",
"options": {
"include": [
"description",
"company",
"stats",
"requester"
]
}
},
"type": "n8n-nodes-base.freshdeskTool",
"typeVersion": 1,
"position": [
17632,
5648
],
"id": "99daa1c9-ebc7-4d89-85bb-357fc65681f8",
"name": "Freshdesk - general support",
"credentials": {
"freshdeskApi": {
"id": "eU5oyV62tEJdY6dV",
"name": "Freshdesk - general support"
}
}
},
{
"parameters": {
"authentication": "appToken",
"resource": "deal",
"operation": "getAll",
"returnAll": "=",
"filters": {}
},
"type": "n8n-nodes-base.hubspotTool",
"typeVersion": 2.2,
"position": [
17072,
5200
],
"id": "129384cf-27a0-46c6-aee3-c451c4506353",
"name": "Hubspot all deals search",
"credentials": {
"hubspotAppToken": {
"id": "DJFxH3uski1VveCl",
"name": "HubSpot account"
}
}
},
{
"parameters": {
"authentication": "appToken",
"resource": "company",
"operation": "getAll",
"returnAll": "=",
"options": {}
},
"type": "n8n-nodes-base.hubspotTool",
"typeVersion": 2.2,
"position": [
16896,
5200
],
"id": "c9e8afe7-9b53-4b70-8783-eb5f56842275",
"name": "Hubspot all companies search",
"credentials": {
"hubspotAppToken": {
"id": "DJFxH3uski1VveCl",
"name": "HubSpot account"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT\n id, excel_row_number, entity, deal_type, invoice_status,\n bill_freq, qb_item_type, close_date, bill_date, hs_customer,\n icon_id, qb_customer, qb_item_description, currency,\n amount_lcy, net_terms, tax, note\nFROM cust_fact_table\nWHERE\n UPPER(TRIM(invoice_status)) = 'UNBILLED'\n AND bill_date BETWEEN '{{ $fromAI('startDate', 'Start of bill date range in YYYY-MM-DD format', 'string') }}'::date\n AND '{{ $fromAI('endDate', 'End of bill date range in YYYY-MM-DD format', 'string') }}'::date\n AND entity IS NOT NULL\n AND amount_lcy IS NOT NULL\n AND amount_lcy <> 0\n AND COALESCE(run_status, '') <> 'created'\n AND COALESCE(on_hold, false) = false\nORDER BY entity, bill_date",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
17280,
5200
],
"id": "71698190-a071-4b15-b5c7-2bad605c033f",
"name": "Read Customer Fact Table for Billing",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"toolDescription": "Update a MUS customer (realm 9341452327761715). Requires Customer_ID and SyncToken from a prior query. Sparse update — only fields passed are changed. Supports: CompanyName, FullyQualifiedName, PrimaryEmailAddr, PrimaryPhone, Notes, BillAddr fields.\n\nMultiple email contacts can be added to PrimaryEmailAddr using comma separation i.e. 1@abc.com, 2@abc.com, 3@abc.com",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/customer?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('Customer_ID', 'The QuickBooks ID of the customer to update', 'string'), SyncToken: $fromAI('SyncToken', 'SyncToken from a prior query — required for updates', 'string'), sparse: true }; const companyName = $fromAI('Company_Name', 'Optional new company name, empty to skip', 'string'); const fqn = $fromAI('Fully_Qualified_Name', 'Optional new fully qualified name, empty to skip', 'string'); const email = $fromAI('Primary_Email_Address', 'Optional new primary email, empty to skip', 'string'); const phone = $fromAI('Primary_Phone', 'Optional new primary phone, empty to skip', 'string'); const notes = $fromAI('Notes', 'Optional customer note (appears on profile and invoices, max 4000 chars), empty to skip', 'string'); const line1 = $fromAI('Line_1', 'Optional billing address line 1, empty to skip', 'string'); const city = $fromAI('City', 'Optional billing address city, empty to skip', 'string'); const postalCode = $fromAI('Postal_Code', 'Optional billing address postal code, empty to skip', 'string'); const state = $fromAI('Country_Subdivision_Code', 'Optional state/province code, empty to skip', 'string'); if (companyName && companyName.trim()) p.CompanyName = companyName.trim(); if (fqn && fqn.trim()) p.FullyQualifiedName = fqn.trim(); if (email && email.trim()) p.PrimaryEmailAddr = { Address: email.trim() }; if (phone && phone.trim()) p.PrimaryPhone = { FreeFormNumber: phone.trim() }; if (notes && notes.trim()) p.Notes = notes.trim(); if (line1 || city || postalCode || state) { p.BillAddr = {}; if (line1 && line1.trim()) p.BillAddr.Line1 = line1.trim(); if (city && city.trim()) p.BillAddr.City = city.trim(); if (postalCode && postalCode.trim()) p.BillAddr.PostalCode = postalCode.trim(); if (state && state.trim()) p.BillAddr.CountrySubDivisionCode = state.trim(); } return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17088,
5568
],
"id": "1f695e1a-d0a9-45a9-9d57-9e9049b7e13d",
"name": "MUS - Update a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Update a MMX customer (realm 9341452328017372). Requires Customer_ID and SyncToken from a prior query. Sparse update — only fields passed are changed. Supports: CompanyName, FullyQualifiedName, PrimaryEmailAddr, PrimaryPhone, Notes, BillAddr fields.\n\nMultiple email contacts can be added to PrimaryEmailAddr using comma separation i.e. 1@abc.com, 2@abc.com, 3@abc.com",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/customer?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('Customer_ID', 'The QuickBooks ID of the customer to update', 'string'), SyncToken: $fromAI('SyncToken', 'SyncToken from a prior query — required for updates', 'string'), sparse: true }; const companyName = $fromAI('Company_Name', 'Optional new company name, empty to skip', 'string'); const fqn = $fromAI('Fully_Qualified_Name', 'Optional new fully qualified name, empty to skip', 'string'); const email = $fromAI('Primary_Email_Address', 'Optional new primary email, empty to skip', 'string'); const phone = $fromAI('Primary_Phone', 'Optional new primary phone, empty to skip', 'string'); const notes = $fromAI('Notes', 'Optional customer note (appears on profile and invoices, max 4000 chars), empty to skip', 'string'); const line1 = $fromAI('Line_1', 'Optional billing address line 1, empty to skip', 'string'); const city = $fromAI('City', 'Optional billing address city, empty to skip', 'string'); const postalCode = $fromAI('Postal_Code', 'Optional billing address postal code, empty to skip', 'string'); const state = $fromAI('Country_Subdivision_Code', 'Optional state/province code, empty to skip', 'string'); if (companyName && companyName.trim()) p.CompanyName = companyName.trim(); if (fqn && fqn.trim()) p.FullyQualifiedName = fqn.trim(); if (email && email.trim()) p.PrimaryEmailAddr = { Address: email.trim() }; if (phone && phone.trim()) p.PrimaryPhone = { FreeFormNumber: phone.trim() }; if (notes && notes.trim()) p.Notes = notes.trim(); if (line1 || city || postalCode || state) { p.BillAddr = {}; if (line1 && line1.trim()) p.BillAddr.Line1 = line1.trim(); if (city && city.trim()) p.BillAddr.City = city.trim(); if (postalCode && postalCode.trim()) p.BillAddr.PostalCode = postalCode.trim(); if (state && state.trim()) p.BillAddr.CountrySubDivisionCode = state.trim(); } return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17072,
6192
],
"id": "8e541a9d-1003-4e4d-9d7f-02083fcceac3",
"name": "MMX - Update a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Update a MSAG customer (realm 9341452373926753). Requires Customer_ID and SyncToken from a prior query. Sparse update — only fields passed are changed. Supports: CompanyName, FullyQualifiedName, PrimaryEmailAddr, PrimaryPhone, Notes, BillAddr fields.\n\nMultiple email contacts can be added to PrimaryEmailAddr using comma separation i.e. 1@abc.com, 2@abc.com, 3@abc.com",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/customer?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('Customer_ID', 'The QuickBooks ID of the customer to update', 'string'), SyncToken: $fromAI('SyncToken', 'SyncToken from a prior query — required for updates', 'string'), sparse: true }; const companyName = $fromAI('Company_Name', 'Optional new company name, empty to skip', 'string'); const fqn = $fromAI('Fully_Qualified_Name', 'Optional new fully qualified name, empty to skip', 'string'); const email = $fromAI('Primary_Email_Address', 'Optional new primary email, empty to skip', 'string'); const phone = $fromAI('Primary_Phone', 'Optional new primary phone, empty to skip', 'string'); const notes = $fromAI('Notes', 'Optional customer note (appears on profile and invoices, max 4000 chars), empty to skip', 'string'); const line1 = $fromAI('Line_1', 'Optional billing address line 1, empty to skip', 'string'); const city = $fromAI('City', 'Optional billing address city, empty to skip', 'string'); const postalCode = $fromAI('Postal_Code', 'Optional billing address postal code, empty to skip', 'string'); const state = $fromAI('Country_Subdivision_Code', 'Optional state/province code, empty to skip', 'string'); if (companyName && companyName.trim()) p.CompanyName = companyName.trim(); if (fqn && fqn.trim()) p.FullyQualifiedName = fqn.trim(); if (email && email.trim()) p.PrimaryEmailAddr = { Address: email.trim() }; if (phone && phone.trim()) p.PrimaryPhone = { FreeFormNumber: phone.trim() }; if (notes && notes.trim()) p.Notes = notes.trim(); if (line1 || city || postalCode || state) { p.BillAddr = {}; if (line1 && line1.trim()) p.BillAddr.Line1 = line1.trim(); if (city && city.trim()) p.BillAddr.City = city.trim(); if (postalCode && postalCode.trim()) p.BillAddr.PostalCode = postalCode.trim(); if (state && state.trim()) p.BillAddr.CountrySubDivisionCode = state.trim(); } return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17104,
6800
],
"id": "74d73667-2416-4e71-a9e7-cb8c9656404b",
"name": "MSAG - Update a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Update a Freshdesk Finance ticket. Use to change status, priority, or subject. Status codes: 2=Open, 3=Pending, 4=Resolved, 5=Closed. Always confirm before closing or resolving. Requires numeric ticket ID from a prior lookup.",
"method": "PUT",
"url": "=https://masttro.freshdesk.com/api/v2/tickets/{{ $fromAI('ticket_id', 'Numeric Freshdesk ticket ID', 'string') }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = {}; const status = $fromAI('status', 'Ticket status: 2=Open, 3=Pending, 4=Resolved, 5=Closed. Pass 0 to skip.', 'number'); const priority = $fromAI('priority', 'Priority: 1=Low, 2=Medium, 3=High, 4=Urgent. Pass 0 to skip.', 'number'); const subject = $fromAI('subject', 'Optional new subject. Empty string to skip.', 'string'); if (status && status > 0) p.status = status; if (priority && priority > 0) p.priority = priority; if (subject && subject.trim()) p.subject = subject.trim(); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17696,
5824
],
"id": "dfe291b7-c3c5-41a6-9916-f83200b97b8d",
"name": "Freshdesk - Update Ticket",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Add a note to a Freshdesk Finance ticket. Private notes are internal only (default). Public notes are visible to the requester. Always retrieve ticket ID via search before calling.",
"method": "POST",
"url": "=https://masttro.freshdesk.com/api/v2/tickets/{{ $fromAI('ticket_id', 'Numeric Freshdesk ticket ID', 'string') }}/notes",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ body: $fromAI('body', 'Note content — plain text or HTML', 'string'), private: $fromAI('private', 'true for internal note, false for customer-visible', 'boolean') ?? true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17840,
5616
],
"id": "8c7fabaa-a457-411e-b569-b24c0ae88fa6",
"name": "Freshdesk - Add Note to Ticket",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
},
"disabled": true
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "pdf-mime-check",
"leftValue": "={{ $json.files?.[0]?.mimeType }}",
"rightValue": "application/pdf",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
15792,
4768
],
"id": "7acb2049-f340-43e8-a691-8feccc8be128",
"name": "PDF upload?"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "merge-chatinput",
"name": "chatInput",
"value": "={{ $('When chat message received').item.json.chatInput }}\n\n--- Uploaded Document ---\n{{ $json.text }}",
"type": "string"
},
{
"id": "pass-session",
"name": "sessionId",
"value": "={{ $('When chat message received').item.json.sessionId }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
16176,
4496
],
"id": "4b87f084-42ce-4f0b-9e60-c0343d6419fa",
"name": "Merge PDF Text"
},
{
"parameters": {
"operation": "pdf",
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
"options": {
"joinPages": true
}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1,
"position": [
16000,
4512
],
"id": "2b54f2f0-d4ec-4ff2-9f26-bdd437b361d4",
"name": "Extract from File"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "excel-mime-check",
"leftValue": "={{ $json.files?.[0]?.mimeType }}",
"rightValue": "vnd\\.ms-excel|spreadsheetml",
"operator": {
"type": "string",
"operation": "regex"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
15968,
4768
],
"id": "ec0ff3a8-459a-47fd-9a0c-307aa8e90167",
"name": "Excel upload?"
},
{
"parameters": {
"operation": "xlsx",
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
"options": {}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1,
"position": [
16128,
4752
],
"id": "c683404a-97b6-4f03-a6f5-827536a28097",
"name": "Extract from File (XLSX)"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "merge-chatinput-xlsx",
"name": "chatInput",
"value": "={{ $('When chat message received').item.json.chatInput }}\n\n--- Uploaded Spreadsheet ---\n{{ JSON.stringify($json, null, 2) }}",
"type": "string"
},
{
"id": "pass-session-xlsx",
"name": "sessionId",
"value": "={{ $('When chat message received').item.json.sessionId }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
16304,
4640
],
"id": "5db4a7bc-0f54-48a0-8fed-4a64860d0efa",
"name": "Merge XLSX Data"
},
{
"parameters": {
"toolDescription": "Create a new Freshdesk Finance ticket. Use when the user asks to create, open, or log a new ticket — NOT when adding to an existing one. Required: subject and description. Optional: priority (1=Low, 2=Medium, 3=High, 4=Urgent), status (2=Open default), email (requester email — omit if unknown), cc_emails (array of strings).\\n\\nAlways post description as HTML for good formatting.\\nDo NOT use this to update or comment on existing tickets — use Freshdesk - Update Ticket or Freshdesk - Add Note to Ticket instead.",
"method": "POST",
"url": "=https://masttro.freshdesk.com/api/v2/tickets/",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { subject: $fromAI('subject', 'Ticket subject line', 'string'), description: $fromAI('description', 'Ticket body — HTML preferred', 'string'), status: $fromAI('status', '2=Open, 3=Pending', 'number') || 2, priority: $fromAI('priority', '1=Low, 2=Medium, 3=High, 4=Urgent', 'number') || 2 }; const email = $fromAI('email', 'Requester email. Empty string for internal tickets.', 'string'); p.email = (email && email.trim()) ? email.trim() : 'mnolan@masttro.com'; return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17856,
5808
],
"id": "6525ca16-86a3-4b47-a5d1-85db4142157e",
"name": "Freshdesk - Create Ticket",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Download a QBO invoice as a PDF and return a clickable download link. Works for MUS, MMX, and MSAG. Pass entity (MUS, MMX, or MSAG) and invoiceId (QBO numeric Id, not DocNumber). Optionally pass a filename. After calling, return the message field verbatim — it contains the clickable download link.",
"method": "POST",
"url": "http://10.116.0.2:5679/webhook/invoice-pdf",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "entity",
"value": "={{ $fromAI('entity', 'Entity code: MUS, MMX, or MSAG', 'string') }}"
},
{
"name": "invoiceId",
"value": "={{ $fromAI('invoiceId', 'QBO numeric Invoice Id (not DocNumber)', 'string') }}"
},
{
"name": "filename",
"value": "={{ $fromAI('filename', 'Optional filename e.g. invoice_MUS_1234.pdf — empty string for auto', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16704,
5216
],
"id": "58a640c1-a7ff-4c5e-8ff8-c84180fc81e8",
"name": "Download Invoice PDF"
},
{
"parameters": {
"toolDescription": "Create a credit memo in MUS (realm 9341452327761715, USD). Use when issuing a credit, refund, or adjustment against a customer account — NOT an invoice.\n\nWorkflow:\n 1. Resolve customerId via MUS - Find customer by name\n 2. Resolve itemId via MUS - Find item by name\n 3. Get next DocNumber: MUS - Get latest invoice DocNumber → +1 → collision-check CreditMemo + Estimate\n 4. Look up taxCodeId: TX or NY state → 'TAX', all others → 'NON'\n 5. Confirm details with user before posting\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, TotalAmt, TxnDate, CustomerRef name. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const TAXABLE_STATES = ['TX', 'NY'];\n const customerState = $fromAI('customerState', 'Two-letter US state code from customer BillAddr.CountrySubDivisionCode. Empty string if unknown.', 'string');\n const stateTaxCode = TAXABLE_STATES.includes((customerState || '').toUpperCase().trim()) ? 'TAX' : 'NON';\n const resolveTaxCode = (id) => { const map = { 'TAX': 'TAX', 'NON': 'NON', '1': 'TAX', '2': 'NON' }; return map[String(id)] || stateTaxCode; };\n const linesRaw = $fromAI('lines', 'JSON array of line items. Each: amount, itemId, description, qty, unitPrice, taxCodeId.', 'string');\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n const p = {\n DocNumber: $fromAI('docNumber', 'Next sequential DocNumber as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Credit memo date YYYY-MM-DD', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'QBO numeric customer Id', 'string') },\n Line: lines.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: resolveTaxCode(l.taxCodeId) }\n }\n }))\n };\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim();\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17168,
5840
],
"id": "2b254fe5-6224-466b-80df-a506beea942d",
"name": "MUS - Create Credit Memo",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a credit memo in MMX (realm 9341452328017372, USD). Use when issuing a credit, refund, or adjustment against a customer account — NOT an invoice.\n\nWorkflow:\n 1. Resolve customerId via MMX - Find customer by name\n 2. Resolve itemId via MMX - Find item by name\n 3. Get next DocNumber: MMX - Get latest invoice DocNumber → +1 → collision-check CreditMemo + Estimate\n 4. MMX customers are non-US — always pass taxCodeId = 'NON'\n 5. Confirm details with user before posting\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, TotalAmt, TxnDate, CustomerRef name. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const linesRaw = $fromAI('lines', 'JSON array of line items. Each: amount, itemId, description, qty, unitPrice. taxCodeId always NON for MMX.', 'string');\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n const p = {\n DocNumber: $fromAI('docNumber', 'Next sequential DocNumber as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Credit memo date YYYY-MM-DD', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'QBO numeric customer Id', 'string') },\n Line: lines.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: 'NON' }\n }\n }))\n };\n const c = $fromAI('currency', 'ISO currency code. Empty string for entity default (USD).', 'string');\n if (c && c.trim()) p.CurrencyRef = { value: c.trim().toUpperCase() };\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim();\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17152,
6368
],
"id": "a61dde4f-5a00-491a-b3b3-3161b3adf864",
"name": "MMX - Create Credit Memo",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a credit memo in MSAG (realm 9341452373926753, CHF). Use when issuing a credit, refund, or adjustment against a customer account — NOT an invoice.\n\nWorkflow:\n 1. Resolve customerId via MSAG - Find customer by name\n 2. Resolve itemId via MSAG - Find item by name\n 3. Get next DocNumber: MSAG - Get latest invoice DocNumber → +1 → collision-check CreditMemo + Estimate\n 4. Query TaxCode (SELECT Id, Name FROM TaxCode WHERE Active = true MAXRESULTS 50) to resolve Swiss VAT code — never hardcode\n 5. Confirm details with user before posting\n 6. After posting, call Get FX Rate (fromCurrency=CHF) to convert amount to USD for reporting\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, TotalAmt, TxnDate, CustomerRef name. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const linesRaw = $fromAI('lines', 'JSON array of line items. Each: amount, itemId, description, qty, unitPrice, taxCodeId (QBO TaxCode Id from TaxCode query — never null or hardcoded).', 'string');\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n const p = {\n DocNumber: $fromAI('docNumber', 'Next sequential DocNumber as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Credit memo date YYYY-MM-DD', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'QBO numeric customer Id', 'string') },\n Line: lines.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: (l.taxCodeId && String(l.taxCodeId).trim()) ? String(l.taxCodeId).trim() : 'NON' }\n }\n }))\n };\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim();\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17136,
7120
],
"id": "0749b243-fada-44d0-a213-fda59f446ea3",
"name": "MSAG - Create Credit Memo",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"contextWindowLength": 10
},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.4,
"position": [
16832,
4928
],
"id": "05180f32-ec76-4a12-9549-e40906cb9e80",
"name": "Simple Memory"
},
{
"parameters": {
"toolDescription": "Apply a credit memo to an open invoice in MUS (realm 9341452327761715). Use when a credit memo exists and needs to be matched against an outstanding invoice — resulting in a $0 payment that closes both.\n\nWorkflow (all steps required):\n 1. Retrieve Credit Memo Id and SyncToken via MUS - Query QBO (QBQL):\n SELECT Id, SyncToken, DocNumber, TotalAmt, RemainingCredit FROM CreditMemo WHERE Id = '<id>' MAXRESULTS 1\n 2. Retrieve the Invoice Id via Postgres view_invoices_open or MUS - Query QBO (QBQL)\n 3. Present credit memo and invoice details to user and confirm before applying\n 4. Call this tool with creditMemoId, syncToken, and invoiceId\n\nThis posts a sparse update to the credit memo with a LinkedTxn pointing to the invoice. QBO will net the credit against the invoice balance automatically.\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, RemainingCredit, and the linked Invoice Id. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('creditMemoId', 'The QBO numeric Credit Memo Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from a prior QBQL query — required for updates', 'string'), sparse: true, Line: [{ Amount: $fromAI('amount', 'Amount to apply — must equal the lesser of the credit memo RemainingCredit and the invoice balance', 'number'), LinkedTxn: [{ TxnId: $fromAI('invoiceId', 'The QBO numeric Invoice Id to apply the credit memo against', 'string'), TxnType: 'Invoice' }] }] }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16912,
5488
],
"id": "0136e7bc-e3bc-42cd-a20c-b23c2a26b6dc",
"name": "MUS - Apply Credit Memo to Invoice",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Apply a credit memo to an open invoice in MMX (realm 9341452328017372). Use when a credit memo exists and needs to be matched against an outstanding invoice — resulting in a $0 payment that closes both.\n\nWorkflow (all steps required):\n 1. Retrieve Credit Memo Id and SyncToken via MMX - Query QBO (QBQL):\n SELECT Id, SyncToken, DocNumber, TotalAmt, RemainingCredit FROM CreditMemo WHERE Id = '<id>' MAXRESULTS 1\n 2. Retrieve the Invoice Id via Postgres view_invoices_open or MMX - Query QBO (QBQL)\n 3. Present credit memo and invoice details to user and confirm before applying\n 4. Call this tool with creditMemoId, syncToken, and invoiceId\n\nThis posts a sparse update to the credit memo with a LinkedTxn pointing to the invoice. QBO will net the credit against the invoice balance automatically.\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, RemainingCredit, and the linked Invoice Id. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('creditMemoId', 'The QBO numeric Credit Memo Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from a prior QBQL query — required for updates', 'string'), sparse: true, Line: [{ Amount: $fromAI('amount', 'Amount to apply — must equal the lesser of the credit memo RemainingCredit and the invoice balance', 'number'), LinkedTxn: [{ TxnId: $fromAI('invoiceId', 'The QBO numeric Invoice Id to apply the credit memo against', 'string'), TxnType: 'Invoice' }] }] }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16944,
6112
],
"id": "ca1e597e-1b04-4202-9fdd-be57905e7f57",
"name": "MMX - Apply Credit Memo to Invoice",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Apply a credit memo to an open invoice in MSAG (realm 9341452373926753). Use when a credit memo exists and needs to be matched against an outstanding invoice — resulting in a CHF 0 payment that closes both.\n\nWorkflow (all steps required):\n 1. Retrieve Credit Memo Id and SyncToken via MSAG - Query QBO (QBQL):\n SELECT Id, SyncToken, DocNumber, TotalAmt, RemainingCredit FROM CreditMemo WHERE Id = '<id>' MAXRESULTS 1\n 2. Retrieve the Invoice Id via Postgres view_invoices_open or MSAG - Query QBO (QBQL)\n 3. Present credit memo and invoice details to user and confirm before applying\n 4. Call this tool with creditMemoId, syncToken, and invoiceId\n\nThis posts a sparse update to the credit memo with a LinkedTxn pointing to the invoice. QBO will net the credit against the invoice balance automatically.\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, RemainingCredit, and the linked Invoice Id. Then call Get FX Rate (fromCurrency=CHF) to convert any amounts to USD for reporting. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('creditMemoId', 'The QBO numeric Credit Memo Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from a prior QBQL query — required for updates', 'string'), sparse: true, Line: [{ Amount: $fromAI('amount', 'Amount to apply — must equal the lesser of the credit memo RemainingCredit and the invoice balance', 'number'), LinkedTxn: [{ TxnId: $fromAI('invoiceId', 'The QBO numeric Invoice Id to apply the credit memo against', 'string'), TxnType: 'Invoice' }] }] }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16960,
7136
],
"id": "13fdf991-1331-4e62-8a17-6bd74143be8e",
"name": "MSAG - Apply Credit Memo to Invoice",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"content": "# Microsoft Teams",
"height": 448,
"width": 768
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
17504,
4640
],
"id": "03baead9-ff4f-47bb-8dd8-48020cfa5834",
"name": "Sticky Note2"
},
{
"parameters": {
"public": true,
"initialMessages": "💰 MoneyBot (READ-ONLY) online.\nI can check QBO customers, find real time FX rates, query Hubspot deals (closed/won), risk score customers with open invoices, create CSV files and query 1,200+ customer contracts and amendments in memory. I can export invoices to PDF.\n\nYou can upload images, XLSX or PDFs for review.",
"availableInChat": true,
"agentName": "Moneybot",
"options": {
"allowFileUploads": true,
"allowedFilesMimeTypes": "*"
}
},
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1.4,
"position": [
15632,
4512
],
"id": "2d461a9b-bb76-44a1-8f19-82bb3d584a84",
"name": "When chat message received",
"webhookId": "moneybot-readonly",
"alwaysOutputData": true
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-5",
"mode": "list",
"cachedResultName": "Claude Sonnet 5"
},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
16848,
4480
],
"id": "884af69f-cde9-4acd-9191-86e34ca41f3e",
"name": "Claude",
"credentials": {
"anthropicApi": {
"id": "uJ4J74wLCEWqVd9B",
"name": "Anthropic - Moneybot"
}
}
},
{
"parameters": {
"model": "gemma4:12b",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [
17040,
4480
],
"id": "071ce6d1-5379-4964-8d10-36682cceadc9",
"name": "Ollama",
"credentials": {
"ollamaApi": {
"id": "PjA3zj5FSQY841F6",
"name": "Ollama 100.111.46.13 fireball"
}
}
}
],
"connections": {
"MMX - Create Invoice via QuickBooks API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Send Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Update Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Find customer by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Find item by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Get latest invoice DocNumber": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Get a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Get an item in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Invoice via QuickBooks API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Send Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Update Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks open invoices": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks customer payments": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks general ledger": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot deals": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MHAG - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MHAG - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MHAG - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MGC - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MGC - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MGC - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres PGVector Store": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"mistral-embed": {
"ai_embedding": [
[
{
"node": "Postgres PGVector Store",
"type": "ai_embedding",
"index": 0
}
]
]
},
"Contract_Direct_Lookup": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Quickbooks Chart of Accounts MGC MUS MMX MSAG MHAG": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams DM Chats — returns all 1:1 chats with id and members. Search members for displayName or email matching the target person. Use chatId with Send Teams DM.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams Group Chats — returns all group chats with id, topic, and members. Match by topic name or member displayName. Use chatId with Send Teams DM.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams List — returns all joined teams with id and displayName. Call this first to resolve a team name to a teamId before calling Get Teams Channels.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams Channels — given a teamId from Get Teams List, returns all channels with id and displayName. Use channelId and teamId together with Send Teams Channel Message.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Send Teams DM": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Send Teams Channel Message": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks balance sheet": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Void Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Void Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Void Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Get a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Get an item in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Invoice via QuickBooks API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Send Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Update Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Export_File": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Get latest invoice DocNumber": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Find item by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Find customer by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"score_customer_risk": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Call 'MoneyBot — Collections Emails'": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Generate EML File": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get FX Rate": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Get an item in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Get a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Get latest invoice DocNumber": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Find item by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Find customer by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk Contact Lookup - Finance": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - finance tickets": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - general support": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot all deals search": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot all companies search": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Read Customer Fact Table for Billing": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Update a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Update a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Update a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - Update Ticket": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - Add Note to Ticket": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"PDF upload?": {
"main": [
[
{
"node": "Extract from File",
"type": "main",
"index": 0
}
],
[
{
"node": "Excel upload?",
"type": "main",
"index": 0
}
]
]
},
"Merge PDF Text": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Extract from File": {
"main": [
[
{
"node": "Merge PDF Text",
"type": "main",
"index": 0
}
]
]
},
"Excel upload?": {
"main": [
[
{
"node": "Extract from File (XLSX)",
"type": "main",
"index": 0
}
],
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Extract from File (XLSX)": {
"main": [
[
{
"node": "Merge XLSX Data",
"type": "main",
"index": 0
}
]
]
},
"Merge XLSX Data": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Freshdesk - Create Ticket": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Download Invoice PDF": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Credit Memo": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create Credit Memo": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Credit Memo": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Simple Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"MUS - Apply Credit Memo to Invoice": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Apply Credit Memo to Invoice": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Apply Credit Memo to Invoice": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"When chat message received": {
"main": [
[
{
"node": "PDF upload?",
"type": "main",
"index": 0
}
]
]
},
"Claude": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
}
},
"settings": {
"binaryMode": "separate",
"errorWorkflow": "tJSgL4iy63ys2NCA",
"availableInMCP": false,
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"nodeGroups": [],
"pinData": {},
"versionId": "dd228b98-e8d3-45e7-8ee7-84865b8ba946",
"activeVersionId": "dd228b98-e8d3-45e7-8ee7-84865b8ba946",
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-05-08T17:09:50.845Z",
"createdAt": "2026-05-08T17:09:50.845Z",
"role": "workflow:owner",
"workflowId": "eoMLOlRefzddaMQs",
"projectId": "X4TTvIwW8wN0Iirz"
}
],
"activeVersion": {
"updatedAt": "2026-07-01T13:25:57.525Z",
"createdAt": "2026-07-01T13:25:53.470Z",
"versionId": "dd228b98-e8d3-45e7-8ee7-84865b8ba946",
"workflowId": "eoMLOlRefzddaMQs",
"nodes": [
{
"parameters": {
"content": "## Internal link using basic auth\n\nhttps://Masttr0!:Masttr0!@moneybot.masttro.com/webhook/672bf651-91e6-41de-b897-13e54c0e1f10/chat\n\nhttps://moneybot.masttro.com/webhook/moneybot-readonly/chat",
"height": 240,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
16000,
3984
],
"id": "d03c3c99-ddd5-4d62-a364-1db45756e92b",
"name": "Sticky Note"
},
{
"parameters": {
"content": "# 🤖 Core — Trigger · Agent · LLMs · Memory\n\nEntry point and the AI brain. Connect your preferred LLM (Sonnet, Haiku, or Ollama) to the agent. Simple Memory provides per-session context.",
"height": 800,
"width": 1868,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15584,
4288
],
"id": "830b99a8-bf8c-4944-aaa8-ee857f93a6be",
"name": "Sticky Core"
},
{
"parameters": {
"options": {
"systemMessage": "=Current datetime is {{ $now }}\n\nYou are MoneyBot, a finance automation assistant for Masttro.\n\n## ENTITIES\nMUS (US, realm 9341452327761715, USD) · MMX (Mexico, realm 9341452328017372, USD) · MSAG (Switzerland, realm 9341452373926753, CHF) · MHAG (Switzerland holding, realm 9341452561201469, CHF — JE only) · MGC (Global, realm 9130354300064436, USD — JE only)\n\n## TOOL SELECTION\n| Intent | Tool |\n|---|---|\n| Open/unpaid invoices | Postgres: view_invoices_open |\n| All invoices by date | ENTITY - Query QBO (QBQL) |\n| Payments received | Postgres: view_payments |\n| GL / revenue / expenses / P&L | Postgres: AI_view_general_ledger |\n| Account ID lookup | Postgres: chartofaccounts — always before JE |\n| HubSpot closed-won deals | Postgres: view_hubspot_closedwon |\n| HubSpot owner lookup | Postgres: hubspot_owners |\n| HubSpot all deals/companies | Fallback if not in view_hubspot_closedwon |\n| Billing schedule | Postgres: view_hubspot_closedwon |\n| Next invoice number | ENTITY - Get latest invoice DocNumber |\n| Find customer | ENTITY - Find customer by name |\n| Find item/product | ENTITY - Find item by name |\n| Ad-hoc QBO query | ENTITY - Query QBO (QBQL) |\n| Customer record by ID | ENTITY - Get a customer |\n| Create / delete / reclass JE | ENTITY - Create/delete Journal Entry |\n| Export to CSV/Excel | Export_File |\n| Contract terms / renewal dates | pgvector store first, fallback Contract_Direct_Lookup |\n| DSO / days to pay | view_payments JOIN view_invoices_open |\n| Account balances / BS / assets / liabilities / equity | Postgres: view_balancesheet |\n| Find tickets | Freshdesk - finance tickets |\n| Update ticket status / priority | Freshdesk - Update Ticket |\n| Add note / internal comment | Freshdesk - Add Note to Ticket |\n| Create new ticket | Freshdesk - Create Ticket |\n| Download invoice as PDF | Download Invoice PDF |\n| Apply credit memo to invoice | ENTITY - Apply Credit Memo to Invoice |\n\nNever dump all invoices, customers, or payments from QBO. Always use Postgres views for list queries.\n\n## POSTGRES RULES\n- AI_view_general_ledger: \"Date\" is a native DATE — always use DATE_TRUNC or CURRENT_DATE, never text strings\n- ALL mixed-case columns must be double-quoted: \"Date\", \"Account\", \"AccountType\", \"AccountSubType\", \"Classification\", \"Transaction_Type\", \"Memo_Description\", \"CustomerName\", \"Customer_Id\", \"Vendor\", \"Vendor_Id\", \"Account_Id\", \"Class\", \"Department\"\n- Lowercase (no quotes): company_id, realm_id, amount, debit, credit, exch_rate, currency\n- Never SELECT * without a WHERE clause — always use ILIKE for case-insensitive searches\n\n## QBQL RULES\nQBQL is NOT SQL. Forbidden: IN(...), OR, LIKE, ILIKE, nested parentheses, subqueries, JOINs, GROUP BY, aggregates.\nUse ORDERBY (one word), MAXRESULTS (not LIMIT, cap 100), single quotes only.\nDate filters: TxnDate >= 'YYYY-MM-DD' is valid. Derive dates from $now — never hardcode year literals.\nMultiple values: run separate calls, never chain OR.\n\nFor all-invoices-by-date, run one call per AR entity (MUS, MMX, MSAG):\n SELECT Id, DocNumber, TxnDate, CustomerRef, TotalAmt, Balance FROM Invoice\n WHERE TxnDate >= '{{ $now.toFormat(\"yyyy-MM-01\") }}' MAXRESULTS 100\n\n## QBO REFERENCE ID RULES — APPLIES TO ALL QBQL QUERIES AND WRITE OPERATIONS\nNever use a customer or item NAME as a filter value for ID-typed fields in QBQL.\nCustomerRef, ItemRef, VendorRef are reference fields — they require numeric IDs, not names.\n\nWRONG: SELECT * FROM Invoice WHERE CustomerRef = 'Ritholtz Wealth Management LLC'\nRIGHT: \n 1. SELECT Id FROM Customer WHERE DisplayName = 'Ritholtz Wealth Management LLC'\n 2. SELECT * FROM Invoice WHERE CustomerRef = '<numeric Id from step 1>'\n\nThis applies to QBQL reads as much as write payloads — resolve the Id first, always.\n\n## INVOICE CREATION WORKFLOW\n1. Find customer → get customerId + BillAddr.CountrySubDivisionCode\n2. Find item → get itemId\n3. Get latest DocNumber → +1 → collision-check CreditMemo + Estimate → use first clear number\n4. Tax code:\n - MUS: state TX or NY → 'TAX'; all others → 'NON'; unknown → 'NON'. Never omit.\n - MMX: always 'NON'\n - MSAG: query TaxCode table for Swiss VAT id\n5. Confirm details with user before creating\n\n## INVOICE SEND / UPDATE RULES\nSend: always use a literal email address — look up PrimaryEmailAddr if user says \"use the one on file\".\nUpdate (sparse): fetch SyncToken → confirm changes → copy lines verbatim from fetched array, substitute only what changed → call Update.\n\n## JOURNAL ENTRY RULES\n- Debits must equal credits — verify before posting\n- Always look up account IDs via chartofaccounts first; always populate Department\n- To modify a JE: delete existing → create corrected. Confirm debit/credit table before posting.\n- operation=delete is a URL parameter — never include in JSON body\n- MHAG/MGC: JE and Query QBO only — no invoice or customer tools\n- Entity currencies: MUS→USD · MMX→USD · MSAG→CHF · MHAG→CHF · MGC→USD\n- DocNumber format: JE-YYYYMM-NNN (max 13 chars). Reclass suffix: append \"-R\" (must stay ≤21 chars total)\n\n## RECLASS JOURNAL ENTRY\n1. Fetch Id + SyncToken via QBQL\n2. Look up accounts via chartofaccounts\n3. Confirm debit/credit table with user\n4. Delete original\n5. Post corrected (DocNumber = original + \"-R\")\n6. Confirm new JE Id\n\n## CURRENCY\nHome currencies: MUS/MMX/MGC=USD · MSAG/MHAG=CHF\nAfter any GL query with MSAG/MHAG rows: call Get FX Rate (fromCurrency=CHF), multiply by rates.USD, report USD only with note \"converted from CHF at X CHF/USD\".\nFor cross-currency deal totals: use amount_in_home_currency.\n\n## CONTRACT RETRIEVAL\n1. pgvector first — use specific clause language, one query per term\n2. If pgvector returns wrong client → Contract_Direct_Lookup\n3. If not found after both: \"This detail was not found in the retrieved contract sections.\"\n\n## BILLING SCHEDULE\nQuery view_hubspot_closedwon. Verify via pgvector if data looks wrong.\nBilling rules: Monthly→one row/month · Quarterly→every 3 months · Semi-annual→every 6 months · Annual→anniversary · NULL term→check pgvector, do not default to Annual.\n\n## LARGE DATASET HANDLING\nPhase 1 — collect silently: all filtering in SQL, LIMIT 50 OFFSET N until 0 rows.\nPhase 2 — present once: markdown tables, max 25 rows. Rows >25: show first 25 + offer export. Rows >100: skip table, call Export_File immediately.\n\n## FILE EXPORT\nFilename: report_type_YYYY-MM-DD (snake_case, derive from $now). Pass filename (no extension) + sql.\nReturn the message field verbatim. Never prefix contracts with schema.\n\n## FRESHDESK\nSearch by keyword: pass raw keyword only — e.g. acacia\nSearch by person: Contact Lookup → get email → requester:'email@domain.com' AND status:2 OR requester:'email@domain.com' AND status:3\nSingle quotes around all string values. Never use parentheses in query strings.\nStatus codes: 2=Open · 3=Pending · 4=Resolved · 5=Closed\nNotes default to private — ask if customer-visible. Always retrieve ticket Id before writing. Post in HTML format.\nConfirm before closing or resolving.\n\n## MICROSOFT TEAMS\nSend only when explicitly asked. Never auto-send after a query.\nIdentity: userId 94387110-efa9-4e7f-8f18-3332df67c0a9 · email mnolan@masttro.com\n1:1 DM: Get Teams DM Chats → find non-self member → Send Teams DM (once)\nGroup: Get Teams Group Chats → match topic/member → Send Teams DM (once)\nChannel: Get Teams List → teamId → Get Teams Channels → channelId → Send Teams Channel Message (once)\nNever guess a chatId. Any send response = done, never retry. Plain text only, max 10 rows.\n\n## OUTPUT FORMAT\n- Natural language and markdown only — no XML, tool syntax, or parameter tags\n- Never surface tool names or selection reasoning\n- Never hardcode year literals in SQL — always derive from NOW()\n- After a successful write action: clean confirmation summary only, no intermediate narration",
"maxIterations": 100,
"passthroughBinaryImages": true,
"batching": {
"batchSize": 1,
"delayBetweenBatches": 2000
}
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
16400,
4448
],
"id": "614a4991-0062-4cbe-bf33-25e8ab667b10",
"name": "AI Agent"
},
{
"parameters": {
"content": "# 🇲🇽 MMX — QuickBooks Mexico (MXN)\nRealm: 9341452328017372\n\nRead: Find customer/item, get DocNumber, ad-hoc QBQL\nWrite: Create/update customer · Create/send/update invoice\n\n> Default currency is MXN. Pass currency override if needed.",
"height": 580,
"width": 1636,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
6080
],
"id": "e6717b22-9345-41a2-81e4-05eaae719e9e",
"name": "Sticky MMX"
},
{
"parameters": {
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const c = $fromAI('currency', 'ISO currency code. MMX default is USD. Pass empty string to accept entity default.', 'string');\n const taxCodeMap = { 'TAX': 'TAX', 'NON': 'NON', '1': 'TAX', '2': 'NON' };\n\n const resolveTaxCode = (id) => taxCodeMap[String(id)] || 'NON';\n\n const p = {\n DocNumber: $fromAI('docNumber', 'The next sequential invoice number as a string', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'The numeric QuickBooks customer ID', 'string') },\n Line: JSON.parse($fromAI('lines', 'JSON array string of line items.', 'string')).map(l => ({\n Amount: l.amount ?? (l.qty * l.unitPrice),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty,\n UnitPrice: l.unitPrice,\n TaxCodeRef: { value: resolveTaxCode(l.taxCodeId) }\n }\n }))\n };\n if (c && c.trim()) p.CurrencyRef = { value: c.trim().toUpperCase() };\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
6512
],
"id": "482352cf-27e8-47a7-8194-02294478dfa3",
"name": "MMX - Create Invoice via QuickBooks API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Send a posted MMX invoice by email (realm 9341452328017372). ALWAYS have a literal email address before calling. Pass invoiceId (QBO numeric Id) and sendTo email address.",
"method": "POST",
"url": "=https://quickbooks.api.intuit.com/v3/company/9341452328017372/invoice/{{ $fromAI('invoiceId', 'The QBO numeric Invoice Id (not DocNumber)', 'string') }}/send?sendTo={{ $fromAI('sendTo', 'Recipient email address — must be a real address, never empty', 'string') }}&minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/octet-stream"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
6512
],
"id": "52a8cffc-6914-44b2-8d49-44d163df253a",
"name": "MMX - Send Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Update a posted MMX invoice (realm 9341452328017372). Requires invoiceId and syncToken from a prior QBQL query. Sparse update — only fields passed are changed.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('invoiceId', 'QBO numeric Invoice Id from prior query', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from prior query', 'string'), sparse: true }; const txnDate = $fromAI('txnDate', 'Optional new TxnDate YYYY-MM-DD, empty to skip', 'string'); const dueDate = $fromAI('dueDate', 'Optional new DueDate YYYY-MM-DD, empty to skip', 'string'); const privateNote = $fromAI('privateNote', 'Optional memo, empty to skip', 'string'); const linesJson = $fromAI('lines', 'Optional JSON array of replacement line items, empty string to skip', 'string'); if (txnDate && txnDate.trim()) p.TxnDate = txnDate.trim(); if (dueDate && dueDate.trim()) p.DueDate = dueDate.trim(); if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim(); if (linesJson && linesJson.trim()) p.Line = JSON.parse(linesJson).map(l => ({ Amount: l.amount, DetailType: 'SalesItemLineDetail', Description: l.description, SalesItemLineDetail: { ItemRef: { value: String(l.itemId) }, Qty: l.qty, UnitPrice: l.unitPrice, TaxCodeRef: { value: String(l.taxCodeId) } } })); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
6512
],
"id": "8d926a05-86f0-40b6-96c1-e14fd1484394",
"name": "MMX - Update Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MMX for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim().slice(0, 21);\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
6512
],
"id": "8b351de9-5f60-43d7-b95c-3d9ff4a6c51c",
"name": "MMX - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MMX. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16864,
6400
],
"id": "976533e1-d0d6-477f-b8b8-81567759e930",
"name": "MMX - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"content": "# 🇨🇭 MSAG — QuickBooks Switzerland (CHF)\nRealm: 9341452373926753\n\nRead: Find customer/item, get DocNumber, ad-hoc QBQL\nWrite: Create/update customer · Create/send/update invoice\n\n> Swiss VAT: 8.1% standard · 2.6% reduced · 3.8% lodging · 0% export\n> Always convert CHF→USD via Get FX Rate before reporting.",
"height": 580,
"width": 1620,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
6720
],
"id": "07586a4c-d4ef-4ec6-af83-241ae97685c2",
"name": "Sticky MSAG"
},
{
"parameters": {
"toolDescription": "Find MSAG customer(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, DisplayName, CompanyName, PrimaryEmailAddr, Active FROM Customer WHERE DisplayName LIKE '%{{ $fromAI('name_fragment', 'Substring of the customer name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
6832
],
"id": "3a01b6d9-217f-451c-bb4d-0748c3a4fba7",
"name": "MSAG - Find customer by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Find MSAG item(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, Name, Description, UnitPrice, Type, Active FROM Item WHERE Name LIKE '%{{ $fromAI('name_fragment', 'Substring of the item name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
6832
],
"id": "ab6b639a-3991-460b-a5da-e3f5c3a1968e",
"name": "MSAG - Find item by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Returns the single most recently created Invoice in MSAG. Use to determine next sequential invoice number.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "SELECT DocNumber, TxnDate, MetaData FROM Invoice ORDERBY MetaData.CreateTime DESC MAXRESULTS 1"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
6832
],
"id": "c9dea8ca-b9bf-4dd7-a7e5-09e2ac05c5cd",
"name": "MSAG - Get latest invoice DocNumber",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MSAG QuickBooks (realm 9341452373926753). ALWAYS include WHERE and MAXRESULTS. Use SELECT Id, Name, Description, Active FROM TaxCode WHERE Active = true MAXRESULTS 50 to discover Swiss VAT codes.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
6832
],
"id": "7fdec87b-3bbd-4a33-8dc9-b4de53054520",
"name": "MSAG - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"customerId": "={{ $fromAI('Customer_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15680,
6992
],
"id": "05ff2df0-96b4-482c-9120-0c81af2e1cde",
"name": "MSAG - Get a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"resource": "item",
"itemId": "={{ $fromAI('Item_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15936,
6992
],
"id": "dcd00d78-c71e-479a-b1d3-61230b07cf01",
"name": "MSAG - Get an item in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({\n DocNumber: $fromAI('docNumber', 'The next sequential invoice number as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Invoice date in YYYY-MM-DD format', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'The numeric QuickBooks customer ID', 'string') },\n Line: (() => {\n const raw = $fromAI('lines', 'JSON array of line items. Each object must have: amount (number), itemId (string), description (string), qty (number), unitPrice (number), taxCodeId (string — use the QBO TaxCode Id from the TaxCode query, never null)', 'string');\n const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;\n return parsed.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: (l.taxCodeId && String(l.taxCodeId).trim()) ? String(l.taxCodeId).trim() : 'NON' }\n }\n }));\n })()\n}) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
7152
],
"id": "eed664c9-00ff-466c-ba83-4e9dfa929552",
"name": "MSAG - Create Invoice via QuickBooks API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Send a posted MSAG invoice by email (realm 9341452373926753). ALWAYS have a literal email address before calling — look it up from the customer record if needed. Pass invoiceId (QBO numeric Id, not DocNumber) and sendTo email address.",
"method": "POST",
"url": "=https://quickbooks.api.intuit.com/v3/company/9341452373926753/invoice/{{ $fromAI('invoiceId', 'The QBO numeric Invoice Id (not DocNumber) from a prior query', 'string') }}/send?sendTo={{ $fromAI('sendTo', 'Recipient email address — must be a real address, never empty', 'string') }}&minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/octet-stream"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
7152
],
"id": "47d35382-faf3-4b3d-a29c-a057e98f59cd",
"name": "MSAG - Send Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
}
},
{
"parameters": {
"toolDescription": "Update a posted MSAG invoice (realm 9341452373926753). Requires invoiceId and syncToken from a prior QBQL query. Sparse update — only fields passed are changed.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('invoiceId', 'QBO numeric Invoice Id from prior query', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from prior query', 'string'), sparse: true }; const txnDate = $fromAI('txnDate', 'Optional new TxnDate YYYY-MM-DD, empty to skip', 'string'); const dueDate = $fromAI('dueDate', 'Optional new DueDate YYYY-MM-DD, empty to skip', 'string'); const privateNote = $fromAI('privateNote', 'Optional memo, empty to skip', 'string'); const linesJson = $fromAI('lines', 'Optional JSON array of replacement line items, empty string to skip', 'string'); if (txnDate && txnDate.trim()) p.TxnDate = txnDate.trim(); if (dueDate && dueDate.trim()) p.DueDate = dueDate.trim(); if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim(); if (linesJson && linesJson.trim()) p.Line = JSON.parse(linesJson).map(l => ({ Amount: l.amount, DetailType: 'SalesItemLineDetail', Description: l.description, SalesItemLineDetail: { ItemRef: { value: String(l.itemId) }, Qty: l.qty, UnitPrice: l.unitPrice, TaxCodeRef: { value: String(l.taxCodeId) } } })); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
7152
],
"id": "1fddb639-ac49-48c0-af37-988dd09a1ef7",
"name": "MSAG - Update Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MSAG for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim().slice(0, 21);\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
7152
],
"id": "e07b778b-2fab-48e0-9660-f022df3887d0",
"name": "MSAG - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MSAG. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16624,
7152
],
"id": "595d91fb-ef4a-4d06-891d-3e67306659d3",
"name": "MSAG - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"content": "# 🗄 Postgres — Data Warehouse (preferred read path)\nCredential: Postgres n8n-quickbooks\n\nviews: open invoices · payments · general ledger · hubspot_deals\n\n> Always use these views for list queries — never dump from QBO directly.\n> Quote mixed-case column names.\n> Revenue in GL is negative — use ABS(SUM(amount)).",
"height": 260,
"width": 1588
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
7360
],
"id": "f6a2fe13-fafb-443a-9512-ce4907bdd22d",
"name": "Sticky Postgres"
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the view_invoices_open Postgres view. Returns UNPAID AR invoices only (balance > 0) — paid invoices excluded. For all invoices by date use ENTITY - Query QBO (QBQL) instead.\n\nAll columns lowercase, no quoting needed.\nColumns: invoice_key, company_id, docnumber, txndate, customerref_name, duedate, totalamt, balance, currencyref, exch_rate\n\nCRITICAL column types:\n txndate — native DATE. Never compare to text string.\n RIGHT: txndate = CURRENT_DATE\n RIGHT: txndate >= CURRENT_DATE - INTERVAL '7 days'\n WRONG: txndate = '2026-05-22' ← TYPE ERROR\n duedate — native DATE. Same rules.\n RIGHT: duedate < CURRENT_DATE\n WRONG: duedate < '2026-05-22' ← TYPE ERROR\n totalamt, balance — numeric\n All others — text\n\nExamples:\n -- Overdue 30+ days\n SELECT company_id, customerref_name, docnumber, duedate, balance, currencyref\n FROM view_invoices_open\n WHERE company_id IN ('MUS','MMX','MSAG')\n AND duedate <= CURRENT_DATE - INTERVAL '30 days'\n ORDER BY duedate ASC LIMIT 100\n\n -- Due in next 30 days\n SELECT company_id, customerref_name, docnumber, duedate, balance\n FROM view_invoices_open\n WHERE company_id IN ('MUS','MMX','MSAG')\n AND duedate >= CURRENT_DATE AND duedate <= CURRENT_DATE + INTERVAL '30 days'\n ORDER BY duedate ASC LIMIT 100\n\n -- AR aging summary\n SELECT company_id, customerref_name, SUM(balance) AS total_balance\n FROM view_invoices_open\n WHERE company_id IN ('MUS','MMX','MSAG')\n GROUP BY company_id, customerref_name\n ORDER BY total_balance DESC LIMIT 20",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against view_invoices_open. MUST include WHERE and LIMIT (<=500). txndate and duedate are native DATE — use CURRENT_DATE arithmetic, never quoted strings.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16224,
7456
],
"id": "f0df476e-8098-4f20-8a0b-5ed08eb2dc14",
"name": "Postgres Quickbooks open invoices",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the view_payments Postgres view for QuickBooks payments across all entities.\n\nCRITICAL: ALL columns except company_id are mixed-case — MUST be double-quoted.\n\nColumn types:\n \"TxnDate\" — native DATE. Never compare to text string.\n RIGHT: \"TxnDate\" >= CURRENT_DATE - INTERVAL '7 days'\n WRONG: \"TxnDate\" >= '2026-05-13' ← TYPE ERROR\n \"TotalAmt\" — numeric\n \"IsCustomerPayment\" — boolean\n All others — text\n\nColumns: company_id, \"TxnDate\", \"CustomerRef_Name\", \"TotalAmt\", \"CurrencyRef\", \"IsCustomerPayment\", \"PaymentMethodDerived\", \"PaymentRefNum\"\n\nExample:\n SELECT company_id, \"TxnDate\", \"CustomerRef_Name\", \"TotalAmt\", \"CurrencyRef\"\n FROM view_payments\n WHERE \"IsCustomerPayment\" = true\n AND \"TxnDate\" >= CURRENT_DATE - INTERVAL '7 days'\n AND company_id IN ('MUS','MMX','MSAG')\n ORDER BY \"TxnDate\" DESC LIMIT 100\n\nDSO: both \"TxnDate\" and txndate (from view_invoices_open) are native DATE — subtraction works directly, no cast needed.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against view_payments. MUST include WHERE and LIMIT (<=500). All mixed-case columns must be double-quoted. \"TxnDate\" is native DATE — use CURRENT_DATE arithmetic, never quoted strings.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16384,
7456
],
"id": "a00978e0-c87f-4a9b-b1ab-b206523a0a15",
"name": "Postgres Quickbooks customer payments",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the AI_view_general_ledger Postgres view for GL, P&L, revenue, expenses.\n\"Date\" is native DATE — use DATE_TRUNC or CURRENT_DATE arithmetic only, never quoted strings.\nRevenue posts negative — use ABS(SUM(amount)).\nFor expenses: SUM(amount) for net (handles reversals). Never SUM(\"Debit\") alone.\nMust include WHERE and LIMIT (<=500). Double-quote all mixed-case columns.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against \"AI_view_general_ledger\". Must include WHERE and LIMIT (<=500). \"Date\" is native DATE — use DATE_TRUNC or CURRENT_DATE arithmetic, NEVER quoted string literals like \\'2026-01-01\\'. Double-quote all mixed-case column names.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16656,
7456
],
"id": "cc2e4931-5daf-4c5a-b959-8752835d444f",
"name": "Postgres Quickbooks general ledger",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
},
"disabled": true
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query HubSpot deals and closed-won data from Postgres. Use view_hubspot_closedwon for closed-won deals and billing schedules. Use hubspot_deals for pipeline/win-loss analysis.\n\n### view_hubspot_closedwon\nAll lowercase. Already filtered to is_closed_won = true — do NOT add again.\ncontract_start_date / contract_end_date are native DATE — no cast needed.\nALWAYS include deal_url when presenting results.\n\nColumns: deal_id, deal_name, amount, amount_in_home_currency, deal_currency_code,\n close_date, contract_start_date, contract_end_date, contract_term,\n primary_associated_company, company_name, icon_alias, icon_id, icon_client_name,\n client_tier, health_status, renewal_date, churn_notice_date, churn_effective_date,\n onboarding_owner, client_success_manager, masttro_invoicing_region,\n new_pricing_model, deal_url, owner_id\n\n### hubspot_deals\nAll lowercase. Key columns: deal_id, deal_name, amount, amount_in_home_currency,\n close_date, deal_stage, pipeline, owner_id, is_closed_won, is_closed_lost,\n deal_type, contract_start_date, contract_end_date, contract_term,\n churn_date, churn_notice_date, primary_associated_company\nNote: no deal_url or deal_currency_code here — use view_hubspot_closedwon for those.\nColumn is deal_name — never dealname.\namount_in_home_currency is USD — use for cross-currency aggregations.\n\n### hubspot_owners\nColumns: owner_id, firstname, lastname, email\nJOIN: LEFT JOIN hubspot_owners o ON d.owner_id = o.owner_id\n\nBilling schedule standard query:\n SELECT deal_name, primary_associated_company, contract_term,\n contract_start_date, contract_end_date,\n amount, amount_in_home_currency, deal_currency_code, deal_id, deal_url\n FROM view_hubspot_closedwon\n WHERE contract_start_date <= (DATE_TRUNC('year', NOW()) + INTERVAL '1 year - 1 day')::date\n AND (contract_end_date >= DATE_TRUNC('year', NOW())::date OR contract_end_date IS NULL)\n ORDER BY contract_start_date ASC",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against view_hubspot_closedwon, hubspot_deals, or hubspot_owners. MUST include WHERE and LIMIT (<=500).', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
17152,
4912
],
"id": "35c66d65-cc08-4358-946e-5762e372f41c",
"name": "Hubspot deals",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"content": "# 🏦 MHAG & MGC — Journal Entry Only\nMHAG realm: 9341452561201469 (CHF)\nMGC realm: 9130354300064436 (USD)\n\nThese entities have **no invoice or customer tools**.\nAvailable: Create Journal Entry · delete Journal Entry · Query QBO (QBQL)\n\n> Use Query QBO to find Account IDs before posting.\n> MHAG amounts are CHF — convert via Get FX Rate before reporting.",
"height": 260,
"width": 1684,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
7712
],
"id": "d59faace-9090-4754-bdb6-8959ae321347",
"name": "Sticky MHAG MGC"
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MHAG for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452561201469/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (privateNote && privateNote.trim()) payload.PrivateNote = privateNote.trim();\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16736,
7792
],
"id": "3b159143-6489-4afa-923d-25784b80d894",
"name": "MHAG - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "vE8iWZDtmiPKgGao",
"name": "QuickBooks Online MHAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MHAG QuickBooks (realm 9341452561201469). ALWAYS include WHERE and MAXRESULTS.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452561201469/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16176,
7792
],
"id": "82a645c5-0635-4810-9912-0831f6dfc6f6",
"name": "MHAG - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "vE8iWZDtmiPKgGao",
"name": "QuickBooks Online MHAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MHAG. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452561201469/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16928,
7792
],
"id": "8a310b53-142a-4d42-867c-d692fd8edd4d",
"name": "MHAG - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "vE8iWZDtmiPKgGao",
"name": "QuickBooks Online MHAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MGC for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9130354300064436/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim();\n if (privateNote && privateNote.trim()) payload.PrivateNote = privateNote.trim();\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16368,
7792
],
"id": "c8e3f85b-6de4-4cee-bf17-e6273d4a83e1",
"name": "MGC - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "bm9Oc2QzBM8bx9YW",
"name": "QuickBooks Online MGC"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MGC QuickBooks (realm 9130354300064436). ALWAYS include WHERE and MAXRESULTS.",
"url": "https://quickbooks.api.intuit.com/v3/company/9130354300064436/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16560,
7792
],
"id": "930fb4f2-58f1-4494-bcd7-175802124a82",
"name": "MGC - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "bm9Oc2QzBM8bx9YW",
"name": "QuickBooks Online MGC"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MGC. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9130354300064436/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17120,
7792
],
"id": "4036d440-4e01-4ec7-bf11-d4ff0b157343",
"name": "MGC - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "bm9Oc2QzBM8bx9YW",
"name": "QuickBooks Online MGC"
}
},
"disabled": true
},
{
"parameters": {
"mode": "retrieve-as-tool",
"toolDescription": "Contract terms, payment schedules, billing frequency, SLA, termination clauses, effective dates — Postgres PGVector contract store.\n\nCRITICAL: Always use specific clause language in queries, NOT generic client+contract phrases.\nGOOD queries: \"semi-annual invoice payment schedule\", \"annual license fee payment timing\", \"termination notice period\"\nBAD queries: \"49 management contract terms\", \"client contract\"\n\nUse this tool first for any contract lookup. If results return chunks from the wrong client, immediately fall back to Contract_Direct_Lookup.",
"tableName": "contracts",
"topK": 10,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.vectorStorePGVector",
"typeVersion": 1.3,
"position": [
17184,
4704
],
"id": "2f902fe9-23ea-4ebb-b953-c22f98cb9ee1",
"name": "Postgres PGVector Store",
"credentials": {
"postgres": {
"id": "YrHntSw7KF7Za0tK",
"name": "Postgres vectors"
}
}
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.embeddingsMistralCloud",
"typeVersion": 1,
"position": [
17296,
4912
],
"id": "545fd8be-c97c-41f2-9d72-963bbbc15c74",
"name": "mistral-embed",
"credentials": {
"mistralCloudApi": {
"id": "lHc1GsczHEq7NL55",
"name": "Mistral - contracts vector"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Direct SQL fallback for contract lookups. Use when pgvector returns chunks from the wrong client.\n\nTable: contracts (never vectors.contracts or public.contracts)\nColumns: id, text, metadata, client_name, contract_type, effective_date, entity, currency, section_heading, page_number, filename\nUse page_number = 1 to get one row per contract.\n\nExample:\n SELECT text, section_heading, page_number FROM contracts\n WHERE client_name ILIKE '%49 Management%'\n ORDER BY page_number ASC, id ASC LIMIT 50\n\nFor contract list:\n SELECT client_name, contract_type, entity, currency, effective_date\n FROM contracts WHERE page_number = 1\n ORDER BY client_name ASC LIMIT 200",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against the contracts table. Always filter by client_name using ILIKE. Always include ORDER BY page_number ASC, id ASC and a LIMIT.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16992,
4912
],
"id": "710e5a0d-2688-4335-80a8-74bf56246f7a",
"name": "Contract_Direct_Lookup",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Look up QuickBooks account IDs from the chartofaccounts Postgres table. ALWAYS use this before creating any journal entry — never use QBQL Account queries for ID lookups.\n\nKey columns: company_id, account_id (text — pass as-is in AccountRef), \"Name\", \"AcctNum\", \"Active\", \"Classification\", \"AccountType\"\n\nRules:\n- account_id is text — never cast it\n- Always filter by company_id\n- For multi-entity JEs: run one query per entity or use company_id IN (...)\n\nLookup by AcctNum:\n SELECT company_id, account_id, \"Name\", \"AcctNum\", \"Classification\"\n FROM chartofaccounts\n WHERE company_id = 'MUS' AND \"Active\" = true AND \"AcctNum\" ILIKE '%41440%'\n LIMIT 10\n\nLookup by name:\n SELECT company_id, account_id, \"Name\", \"AcctNum\", \"Classification\", \"AccountType\"\n FROM chartofaccounts\n WHERE company_id = 'MUS' AND \"Active\" = true AND \"Name\" ILIKE '%Bonus Expense%'\n LIMIT 10\n\nLookup multiple AcctNums at once:\n SELECT company_id, account_id, \"Name\", \"AcctNum\"\n FROM chartofaccounts\n WHERE company_id = 'MUS' AND \"Active\" = true AND (\"AcctNum\" ILIKE '%41440%' OR \"AcctNum\" ILIKE '%11210%')\n LIMIT 20\n\nLookup across entities:\n SELECT company_id, account_id, \"Name\", \"AcctNum\"\n FROM chartofaccounts\n WHERE company_id IN ('MUS','MMX','MSAG') AND \"Active\" = true AND \"AcctNum\" ILIKE '%41440%'\n LIMIT 20",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against chartofaccounts. Always filter by company_id. Always include LIMIT. Double-quote mixed-case columns: \"Name\", \"AcctNum\", \"Active\", \"Classification\", \"AccountType\". account_id is lowercase and unquoted.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
16912,
7456
],
"id": "2def1f16-acee-43f9-a6b3-29e86331cac7",
"name": "Quickbooks Chart of Accounts MGC MUS MMX MSAG MHAG",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
},
"disabled": true
},
{
"parameters": {
"url": "https://graph.microsoft.com/v1.0/me/chats?$filter=chatType eq 'oneOnOne'&$expand=members&$select=id,chatType&$top=50",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17584,
4768
],
"id": "df489b7b-148f-400d-a1c0-1763df7e6b7a",
"name": "Get Teams DM Chats — returns all 1:1 chats with id and members. Search members for displayName or email matching the target person. Use chatId with Send Teams DM.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"url": "https://graph.microsoft.com/v1.0/me/chats?$filter=chatType eq 'group'&$expand=members&$select=id,chatType,topic&$top=50",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17856,
4928
],
"id": "537dd0af-08ea-4de3-b457-0c1378ccab70",
"name": "Get Teams Group Chats — returns all group chats with id, topic, and members. Match by topic name or member displayName. Use chatId with Send Teams DM.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"url": "https://graph.microsoft.com/v1.0/me/joinedTeams?$select=id,displayName",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17584,
4928
],
"id": "217303ac-0fc1-44f8-b035-ed629cfc734a",
"name": "Get Teams List — returns all joined teams with id and displayName. Call this first to resolve a team name to a teamId before calling Get Teams Channels.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"url": "=https://graph.microsoft.com/v1.0/teams/{{ $fromAI('teamId', 'The team GUID from Get Teams List matching the target team displayName', 'string') }}/channels?$select=id,displayName",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "microsoftTeamsOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17824,
4768
],
"id": "49d75f49-785a-49f1-a28a-2b623d2d1faf",
"name": "Get Teams Channels — given a teamId from Get Teams List, returns all channels with id and displayName. Use channelId and teamId together with Send Teams Channel Message.",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"resource": "chatMessage",
"chatId": {
"__rl": true,
"value": "={{ $fromAI('chatId', 'The Teams chat GUID resolved from Get Teams DM Chats or Get Teams Group Chats. For 1:1 chats, find the chat where the member whose userId is NOT 94387110-efa9-4e7f-8f18-3332df67c0a9 matches the target by email or displayName. Never guess — always resolve first.', 'string') }}",
"mode": "id"
},
"message": "={{ $fromAI('message', 'Plain text message to send. No markdown tables. Use line breaks for structure. Summarize large datasets and offer export links rather than pasting rows.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.microsoftTeamsTool",
"typeVersion": 2,
"position": [
18016,
4752
],
"id": "7680071f-4d20-45b1-903c-cf4d3d56bf9d",
"name": "Send Teams DM",
"webhookId": "d0bf3351-3696-4ea0-adbd-ddec03926981",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"resource": "channelMessage",
"teamId": {
"__rl": true,
"value": "={{ $fromAI('teamId', 'The team GUID from Get Teams List matching the target team displayName', 'string') }}",
"mode": "id"
},
"channelId": {
"__rl": true,
"value": "={{ $fromAI('channelId', 'The channel GUID from Get Teams Channels matching the target channel displayName', 'string') }}",
"mode": "id"
},
"message": "={{ $fromAI('message', 'Plain text message to post. No markdown tables. Use line breaks for structure. Summarize large datasets and offer export links rather than pasting rows.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.microsoftTeamsTool",
"typeVersion": 2,
"position": [
18048,
4912
],
"id": "03b2b68c-19a6-4cbe-ac06-ee2e3749691b",
"name": "Send Teams Channel Message",
"webhookId": "fb75f216-5c71-4ea0-867b-90abb819a51c",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account - Mike"
}
}
},
{
"parameters": {
"operation": "create",
"displayName": "={{ $fromAI('Display_Name', `The customer's full name or company name`, 'string') }}",
"additionalFields": {
"PrimaryEmailAddr": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Primary_Email_Address', ``, 'string') }}",
"PrimaryPhone": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Primary_Phone', ``, 'string') }}"
}
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
16896,
6800
],
"id": "589485a7-2896-4635-8324-713bc9e2d93f",
"name": "MSAG - Create customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the \"view_balancesheet\" Postgres view for balance sheet data — Assets, Liabilities, and Equity as of a specific date.\n\nThis view represents CUMULATIVE balances (sum of all transactions up to a point in time), not period activity. Use this for: account balances, net assets, total liabilities, equity position, cash balances, AR/AP balances, and any question about what something IS WORTH or OWES as of a date.\n\nFull column list:\n company_id (text), realm_id (text), report_name (text),\n start_period (text), end_period (text), currency (text),\n accounting_method (text), report_run_at (timestamptz),\n section_path (text), row_group (text), row_type (text),\n row_label (text), account_id (text), depth (integer),\n col_title (text), col_start_date (text), col_end_date (text),\n col_amount (numeric), \"Account Key\" (text), account_name (text)\n\nKEY CONCEPTS:\n - col_amount is the balance AS OF col_end_date (cumulative, not period delta)\n - section_path identifies the BS section: 'Assets', 'Liabilities', 'Equity'\n - row_type = 'Data' for actual account rows; always filter row_type = 'Data' to avoid double-counting totals/headers\n - row_label = human-readable account name\n - depth = hierarchy level (lower = summary, higher = detail)\n - col_end_date is TEXT stored as YYYY-MM-DD — always cast with ::date for comparisons\n\nDATE FILTERING (col_end_date is TEXT — always cast):\n RIGHT: col_end_date::date <= CURRENT_DATE\n RIGHT: col_end_date::date = DATE_TRUNC('month', NOW())::date - INTERVAL '1 day'\n WRONG: col_end_date <= '2026-01-01' ← text comparison, wrong sort order\n\nTo get the most recent snapshot only:\n Use ORDER BY report_run_at DESC LIMIT N, or filter col_end_date::date = (SELECT MAX(col_end_date::date) FROM view_balancesheet WHERE company_id = '...')\n\nCOMMON QUERY TEMPLATES:\n\nLatest balance sheet detail by section:\n SELECT company_id, section_path, row_label, col_amount, col_end_date\n FROM view_balancesheet\n WHERE row_type = 'Data'\n AND company_id IN ('MUS','MMX','MSAG','MHAG','MGC')\n AND col_end_date::date <= CURRENT_DATE\n ORDER BY company_id, section_path, depth, row_label\n LIMIT 500\n\nTotal assets / liabilities / equity summary:\n SELECT company_id, section_path, SUM(col_amount) AS total\n FROM view_balancesheet\n WHERE row_type = 'Data'\n AND company_id IN ('MUS','MMX','MSAG','MHAG','MGC')\n AND col_end_date::date <= CURRENT_DATE\n GROUP BY company_id, section_path\n ORDER BY company_id, section_path\n LIMIT 100\n\nSpecific account balance (e.g. cash):\n SELECT company_id, row_label, col_amount, col_end_date\n FROM view_balancesheet\n WHERE row_type = 'Data'\n AND row_label ILIKE '%cash%'\n AND company_id IN ('MUS','MMX','MSAG','MHAG','MGC')\n AND col_end_date::date <= CURRENT_DATE\n ORDER BY company_id, col_end_date::date DESC\n LIMIT 100\n\nCURRENCY:\n MSAG and MHAG report in CHF. After any query returning CHF rows: call Get FX Rate (fromCurrency=CHF),\n multiply col_amount by rate, report in USD with note \"converted from CHF at X CHF/USD\".\n\nDO NOT use this view for: revenue, expenses, P&L, or period-over-period income — use AI_view_general_ledger instead.\nDO NOT SUM rows where row_type != 'Data' — header and total rows double-count.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT against \"view_balancesheet\". Must include WHERE and LIMIT (<=500). col_end_date is TEXT — always cast with ::date for date comparisons. Double-quote mixed-case columns like \"Account Key\". Always filter row_type = \\'Data\\' unless intentionally pulling summary totals.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
17088,
7456
],
"id": "8177c53a-6296-4b5a-a383-bc1fc1787d59",
"name": "Postgres Quickbooks balance sheet",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a customer payment in MSAG (realm 9341452373926753, default currency CHF).\n\nWorkflow (follow in order):\n 1. Resolve customerId via MSAG - Find customer by name\n 2. Get open invoice Ids and balances via Postgres view_invoices_open (filter company_id = 'MSAG')\n 3. Validate: totalAmt MUST equal sum of all line amounts — QBO will reject mismatches\n 4. Confirm payment details with user before posting\n\nParameters:\n customerId QBO numeric customer Id\n totalAmt Total payment amount (number) — must match sum of lines\n txnDate Payment date YYYY-MM-DD\n lines JSON array: [{\"invoiceId\": \"123\", \"amount\": 500.00}]\n Pass empty array [] to post as unapplied credit\n currencyRef ISO currency code — empty string for entity default (CHF)\n paymentMethodRef Optional QBO payment method Id — empty string to omit\n depositToAccountRef Optional QBO bank account Id — empty string for Undeposited Funds\n privateNote Optional internal memo — empty string to omit\nAfter posting, call Get FX Rate (fromCurrency=CHF) to convert the amount to USD for any reporting.\n\nAfter a successful POST (Payment.Id present in response), return \nonly: Payment.Id, TotalAmt, TxnDate, CustomerRef name, and linked \ninvoice Id. Do not re-fetch the payment or invoice. Ignore any \nerror fields present in the same response body — treat them as \nnon-fatal noise and never surface raw error JSON to the user.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/payment?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const customerId = $fromAI('customerId', 'QBO numeric customer Id', 'string');\n const totalAmt = $fromAI('totalAmt', 'Total payment amount as a number. MUST equal sum of all line amounts.', 'number');\n const txnDate = $fromAI('txnDate', 'Payment date YYYY-MM-DD', 'string');\n const currencyRef = $fromAI('currencyRef', 'ISO currency code. Pass empty string for entity default.', 'string');\n const paymentMethodRef = $fromAI('paymentMethodRef', 'Optional QBO payment method Id. Empty string to omit.', 'string');\n const depositToAccountRef = $fromAI('depositToAccountRef', 'Optional QBO bank account Id to deposit to. Empty string for Undeposited Funds.', 'string');\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n const linesRaw = $fromAI('lines', 'JSON array linking payment to invoices: [{\"invoiceId\": \"123\", \"amount\": 500.00}]. Empty array [] for unapplied credit.', 'string');\n const payload = { CustomerRef: { value: String(customerId) }, TotalAmt: Number(totalAmt), TxnDate: txnDate };\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n if (lines && lines.length > 0) { payload.Line = lines.map(l => ({ Amount: Number(l.amount), LinkedTxn: [{ TxnId: String(l.invoiceId), TxnType: 'Invoice' }] })); }\n if (currencyRef && currencyRef.trim()) { payload.CurrencyRef = { value: currencyRef.trim().toUpperCase() }; }\n if (paymentMethodRef && paymentMethodRef.trim()) { payload.PaymentMethodRef = { value: paymentMethodRef.trim() }; }\n if (depositToAccountRef && depositToAccountRef.trim()) { payload.DepositToAccountRef = { value: depositToAccountRef.trim() }; }\n if (privateNote && privateNote.trim()) { payload.PrivateNote = privateNote.trim(); }\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16784,
7008
],
"id": "ec41cb54-6c99-4ddc-be76-bfb4fe61371c",
"name": "MSAG - Create Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Void a customer payment in MSAG (realm 9341452373926753). Use to reverse a payment that was posted in error.\n\nQBO does not delete payments — voiding zeroes the amount and marks it void while preserving the transaction record.\n\nSteps (all required):\n 1. Retrieve the payment Id and SyncToken via MSAG - Query QBO (QBQL):\n SELECT Id, SyncToken, TxnDate, TotalAmt FROM Payment WHERE Id = '<id>' MAXRESULTS 1\n 2. Present the payment details to user and confirm before voiding\n 3. Call this tool with Id and SyncToken\n\nThe void is irreversible. Always confirm with user first.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/payment?minorversion=65&operation=void",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('payment_id', 'The QBO numeric Payment Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken — retrieve via QBQL first', 'string'), sparse: true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16640,
6864
],
"id": "19fde7fa-7a54-49f0-bbcc-45f7b23c4d81",
"name": "MSAG - Void Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Void a customer payment in MMX (realm 9341452328017372). Use to reverse a payment that was posted in error.\n\nQBO does not delete payments — voiding zeroes the amount and marks it void while preserving the transaction record.\n\nSteps (all required):\n 1. Retrieve the payment Id and SyncToken via MMX - Query QBO (QBQL):\n SELECT Id, SyncToken, TxnDate, TotalAmt FROM Payment WHERE Id = '<id>' MAXRESULTS 1\n 2. Present the payment details to user and confirm before voiding\n 3. Call this tool with Id and SyncToken\n\nThe void is irreversible. Always confirm with user first.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/payment?minorversion=65&operation=void",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('payment_id', 'The QBO numeric Payment Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken — retrieve via QBQL first', 'string'), sparse: true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16992,
6480
],
"id": "439ff7c5-f393-4637-a3c7-1902e2d1194b",
"name": "MMX - Void Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Void a customer payment in MUS (realm 9341452327761715). Use to reverse a payment that was posted in error.\n\nQBO does not delete payments — voiding zeroes the amount and marks it void while preserving the transaction record.\n\nSteps (all required):\n 1. Retrieve the payment Id and SyncToken via MUS - Query QBO (QBQL):\n SELECT Id, SyncToken, TxnDate, TotalAmt FROM Payment WHERE Id = '<id>' MAXRESULTS 1\n 2. Present the payment details to user and confirm before voiding\n 3. Call this tool with Id and SyncToken\n\nThe void is irreversible. Always confirm with user first.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/payment?minorversion=65&operation=void",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('payment_id', 'The QBO numeric Payment Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken — retrieve via QBQL first', 'string'), sparse: true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16992,
5824
],
"id": "b7152cba-ef41-4dcb-8779-1c9993e3c2c8",
"name": "MUS - Void Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a customer payment in MMX (realm 9341452328017372, default currency USD).\n\nWorkflow (follow in order):\n 1. Resolve customerId via MMX - Find customer by name\n 2. Get open invoice Ids and balances via Postgres view_invoices_open (filter company_id = 'MMX')\n 3. Validate: totalAmt MUST equal sum of all line amounts — QBO will reject mismatches\n 4. Confirm payment details with user before posting\n\nParameters:\n customerId QBO numeric customer Id\n totalAmt Total payment amount (number) — must match sum of lines\n txnDate Payment date YYYY-MM-DD\n lines JSON array: [{\"invoiceId\": \"123\", \"amount\": 500.00}]\n Pass empty array [] to post as unapplied credit\n currencyRef ISO currency code — empty string for entity default (USD)\n paymentMethodRef Optional QBO payment method Id — empty string to omit\n depositToAccountRef Optional QBO bank account Id — empty string for Undeposited Funds\n privateNote Optional internal memo — empty string to omit\n\nAfter a successful POST (Payment.Id present in response), return \nonly: Payment.Id, TotalAmt, TxnDate, CustomerRef name, and linked \ninvoice Id. Do not re-fetch the payment or invoice. Ignore any \nerror fields present in the same response body — treat them as \nnon-fatal noise and never surface raw error JSON to the user.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/payment?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const customerId = $fromAI('customerId', 'QBO numeric customer Id', 'string');\n const totalAmt = $fromAI('totalAmt', 'Total payment amount as a number. MUST equal sum of all line amounts.', 'number');\n const txnDate = $fromAI('txnDate', 'Payment date YYYY-MM-DD', 'string');\n const currencyRef = $fromAI('currencyRef', 'ISO currency code. Pass empty string for entity default.', 'string');\n const paymentMethodRef = $fromAI('paymentMethodRef', 'Optional QBO payment method Id. Empty string to omit.', 'string');\n const depositToAccountRef = $fromAI('depositToAccountRef', 'Optional QBO bank account Id to deposit to. Empty string for Undeposited Funds.', 'string');\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n const linesRaw = $fromAI('lines', 'JSON array linking payment to invoices: [{\"invoiceId\": \"123\", \"amount\": 500.00}]. Empty array [] for unapplied credit.', 'string');\n const payload = { CustomerRef: { value: String(customerId) }, TotalAmt: Number(totalAmt), TxnDate: txnDate };\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n if (lines && lines.length > 0) { payload.Line = lines.map(l => ({ Amount: Number(l.amount), LinkedTxn: [{ TxnId: String(l.invoiceId), TxnType: 'Invoice' }] })); }\n if (currencyRef && currencyRef.trim()) { payload.CurrencyRef = { value: currencyRef.trim().toUpperCase() }; }\n if (paymentMethodRef && paymentMethodRef.trim()) { payload.PaymentMethodRef = { value: paymentMethodRef.trim() }; }\n if (depositToAccountRef && depositToAccountRef.trim()) { payload.DepositToAccountRef = { value: depositToAccountRef.trim() }; }\n if (privateNote && privateNote.trim()) { payload.PrivateNote = privateNote.trim(); }\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16704,
6480
],
"id": "001d5dd2-5824-4219-95ca-c6f1fc855044",
"name": "MMX - Create Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a customer payment in MUS (realm 9341452327761715, default currency USD).\n\nWorkflow (follow in order):\n 1. Resolve customerId via MUS - Find customer by name\n 2. Get open invoice Ids and balances via Postgres view_invoices_open (filter company_id = 'MUS')\n 3. Validate: totalAmt MUST equal sum of all line amounts — QBO will reject mismatches\n 4. Confirm payment details with user before posting\n\nParameters:\n customerId QBO numeric customer Id\n totalAmt Total payment amount (number) — must match sum of lines\n txnDate Payment date YYYY-MM-DD\n lines JSON array: [{\"invoiceId\": \"123\", \"amount\": 500.00}]\n Pass empty array [] to post as unapplied credit\n currencyRef ISO currency code — empty string for entity default (USD)\n paymentMethodRef Optional QBO payment method Id — empty string to omit\n depositToAccountRef Optional QBO bank account Id — empty string for Undeposited Funds\n privateNote Optional internal memo — empty string to omit\n\nAfter a successful POST (Payment.Id present in response), return \nonly: Payment.Id, TotalAmt, TxnDate, CustomerRef name, and linked \ninvoice Id. Do not re-fetch the payment or invoice. Ignore any \nerror fields present in the same response body — treat them as \nnon-fatal noise and never surface raw error JSON to the user.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/payment?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const customerId = $fromAI('customerId', 'QBO numeric customer Id', 'string');\n const totalAmt = $fromAI('totalAmt', 'Total payment amount as a number. MUST equal sum of all line amounts.', 'number');\n const txnDate = $fromAI('txnDate', 'Payment date YYYY-MM-DD', 'string');\n const currencyRef = $fromAI('currencyRef', 'ISO currency code. Pass empty string for entity default.', 'string');\n const paymentMethodRef = $fromAI('paymentMethodRef', 'Optional QBO payment method Id. Empty string to omit.', 'string');\n const depositToAccountRef = $fromAI('depositToAccountRef', 'Optional QBO bank account Id to deposit to. Empty string for Undeposited Funds.', 'string');\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n const linesRaw = $fromAI('lines', 'JSON array linking payment to invoices: [{\"invoiceId\": \"123\", \"amount\": 500.00}]. Empty array [] for unapplied credit.', 'string');\n const payload = { CustomerRef: { value: String(customerId) }, TotalAmt: Number(totalAmt), TxnDate: txnDate };\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n if (lines && lines.length > 0) { payload.Line = lines.map(l => ({ Amount: Number(l.amount), LinkedTxn: [{ TxnId: String(l.invoiceId), TxnType: 'Invoice' }] })); }\n if (currencyRef && currencyRef.trim()) { payload.CurrencyRef = { value: currencyRef.trim().toUpperCase() }; }\n if (paymentMethodRef && paymentMethodRef.trim()) { payload.PaymentMethodRef = { value: paymentMethodRef.trim() }; }\n if (depositToAccountRef && depositToAccountRef.trim()) { payload.DepositToAccountRef = { value: depositToAccountRef.trim() }; }\n if (privateNote && privateNote.trim()) { payload.PrivateNote = privateNote.trim(); }\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16688,
5776
],
"id": "dea99ea2-8e0c-4718-beb1-2f5eff72b58a",
"name": "MUS - Create Customer Payment",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"content": "# 🛠 Shared Tools — FX · EML · Collections · Risk Scoring · File Export\n\nCross-entity utilities available to the agent at all times.",
"height": 240,
"width": 1832,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
5136
],
"id": "cd2b86b1-7002-4c75-a85f-82fc0daf7ab5",
"name": "Sticky Other Tools"
},
{
"parameters": {
"customerId": "={{ $fromAI('Customer_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15680,
5712
],
"id": "c8fe2dc6-a7f3-4919-a41e-c318fef0ac51",
"name": "MUS - Get a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"resource": "item",
"itemId": "={{ $fromAI('Item_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15936,
5712
],
"id": "5ff329b4-a21a-4d36-9bc0-2f6434ca15cb",
"name": "MUS - Get an item in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"operation": "create",
"displayName": "={{ $fromAI('Display_Name', `The customer's full name or company name`, 'string') }}",
"additionalFields": {}
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
16752,
5520
],
"id": "a99476ad-e564-4cb3-bbd6-c869ca3df54b",
"name": "MUS - Create customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const TAXABLE_STATES = ['TX', 'NY'];\n const c = $fromAI('currency', 'ISO currency code. MUS default is USD. Pass empty string to accept entity default.', 'string');\n const customerState = $fromAI('customerState', 'Two-letter US state code from customer BillAddr.CountrySubDivisionCode. Pass empty string if unknown.', 'string');\n\n const stateTaxCode = TAXABLE_STATES.includes((customerState || '').toUpperCase().trim()) ? 'TAX' : 'NON';\n\n const resolveTaxCode = (id) => {\n const map = { 'TAX': 'TAX', 'NON': 'NON', '1': 'TAX', '2': 'NON' };\n return map[String(id)] || stateTaxCode;\n };\n\n const p = {\n DocNumber: $fromAI('docNumber', 'The next sequential invoice number as a string', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'The numeric QuickBooks customer ID', 'string') },\n Line: JSON.parse($fromAI('lines', 'JSON array string of line items.', 'string')).map(l => ({\n Amount: l.amount ?? (l.qty * l.unitPrice),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty,\n UnitPrice: l.unitPrice,\n TaxCodeRef: { value: resolveTaxCode(l.taxCodeId) }\n }\n }))\n };\n if (c && c.trim()) p.CurrencyRef = { value: c.trim().toUpperCase() };\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
5872
],
"id": "3677c975-990e-4f83-9392-20afb217334d",
"name": "MUS - Create Invoice via QuickBooks API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Send a posted MUS invoice by email (realm 9341452327761715). ALWAYS have a literal email address before calling — look it up from the customer record if needed. Pass invoiceId (the QBO numeric Id, not DocNumber) and sendTo email address.",
"method": "POST",
"url": "=https://quickbooks.api.intuit.com/v3/company/9341452327761715/invoice/{{ $fromAI('invoiceId', 'The QBO numeric Invoice Id (not DocNumber) from a prior query', 'string') }}/send?sendTo={{ $fromAI('sendTo', 'Recipient email address — must be a real address, never empty', 'string') }}&minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/octet-stream"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
5872
],
"id": "33c6c517-4056-4493-a1cc-ae381492feb1",
"name": "MUS - Send Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"toolDescription": "Update a posted MUS invoice (realm 9341452327761715). Requires invoiceId and syncToken from a prior QBQL query. Uses sparse update — only fields you pass are changed. Pass lines as full replacement line array if changing line items.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/invoice?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('invoiceId', 'QBO numeric Invoice Id from prior query', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from prior query — required for updates', 'string'), sparse: true }; const txnDate = $fromAI('txnDate', 'Optional new transaction date YYYY-MM-DD, empty string to skip', 'string'); const dueDate = $fromAI('dueDate', 'Optional new due date YYYY-MM-DD, empty string to skip', 'string'); const privateNote = $fromAI('privateNote', 'Optional memo/note, empty string to skip', 'string'); const linesJson = $fromAI('lines', 'Optional JSON array of replacement line items (same format as Create Invoice). Pass empty string to leave lines unchanged.', 'string'); if (txnDate && txnDate.trim()) p.TxnDate = txnDate.trim(); if (dueDate && dueDate.trim()) p.DueDate = dueDate.trim(); if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim(); if (linesJson && linesJson.trim()) p.Line = JSON.parse(linesJson).map(l => ({ Amount: l.amount, DetailType: 'SalesItemLineDetail', Description: l.description, SalesItemLineDetail: { ItemRef: { value: String(l.itemId) }, Qty: l.qty, UnitPrice: l.unitPrice, TaxCodeRef: { value: String(l.taxCodeId) } } })); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
5872
],
"id": "50204815-05ef-44ba-b4c8-6bd270632103",
"name": "MUS - Update Invoice via API",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Creates a Journal Entry in QuickBooks Online MUS for manual financial adjustments. Requires: transaction date and line items (Account ID, Amount, Posting Type). Optional: Doc Number, Notes, Department, or Class IDs. Note: If a Document Number is provided, ensure it is unique; if it already exists in QBO, increment the number by 1 until a unique one is found before finalizing.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/journalentry?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const lines = JSON.parse($fromAI('lines', 'JSON array of journal entry lines. Each line: account_id, posting_type (Debit|Credit), amount, description, entity_type, entity_id, department_id, class_id. Debits MUST equal Credits.', 'string'));\n const txnDate = $fromAI('txn_date', 'Transaction date YYYY-MM-DD', 'string');\n const docNumber = $fromAI('doc_number', 'Optional reference/memo number', 'string');\n const privateNote = $fromAI('private_note', 'Optional internal memo', 'string');\n const currency = $fromAI('currency', 'ISO currency code. Pass empty string to accept default.', 'string');\n const payload = { TxnDate: txnDate, Line: lines.map(l => { const line = { Amount: l.amount, DetailType: 'JournalEntryLineDetail', Description: l.description || '', JournalEntryLineDetail: { PostingType: l.posting_type, AccountRef: { value: String(l.account_id) } } }; if (l.entity_type && l.entity_id) { line.JournalEntryLineDetail.Entity = { Type: l.entity_type, EntityRef: { value: String(l.entity_id) } }; } if (l.department_id && String(l.department_id).trim()) { line.JournalEntryLineDetail.DepartmentRef = { value: String(l.department_id).trim() }; } if (l.class_id && String(l.class_id).trim()) { line.JournalEntryLineDetail.ClassRef = { value: String(l.class_id).trim() }; } return line; }) };\n if (docNumber && docNumber.trim()) payload.DocNumber = docNumber.trim().slice(0, 21);\n if (privateNote && privateNote.trim()) payload.PrivateNote = privateNote.trim();\n if (currency && currency.trim()) payload.CurrencyRef = { value: currency.trim().toUpperCase() };\n return JSON.stringify(payload);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
5872
],
"id": "cfb624f1-4949-41ac-8f37-e657302347b1",
"name": "MUS - Create Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "delete a posted journal entry in MUS. ALWAYS retrieve Id and SyncToken first. Confirm with user before deleting.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/journalentry?minorversion=65&operation=delete",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('je_id', 'The QuickBooks JournalEntry Id', 'string'), SyncToken: $fromAI('sync_token', 'The SyncToken', 'string') }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16848,
5712
],
"id": "6be9cf50-13a3-4729-ae7c-fca8f475e339",
"name": "MUS - delete Journal Entry",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Exports query results to a CSV file and returns a download URL. Pass a complete SELECT SQL query and a filename.\n\nParameters:\n- filename: snake_case name without extension, e.g. hosting_expense_2026-05-23\n- sql: complete SELECT query — no LIMIT unless intentional. Follow all column quoting rules for each view.\n\nNever assemble rows in memory for export — always pass the SQL directly.\nFor GL exports: \"Date\" is native DATE — use DATE_TRUNC or CURRENT_DATE, never quoted string literals.",
"method": "POST",
"url": "http://10.116.0.2:5679/webhook/export-insert",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "filename",
"value": "={{ $fromAI('filename', 'CSV filename without extension, snake_case, e.g. hosting_expense_2026-05-23') }}"
},
{
"name": "sql",
"value": "={{ $fromAI('sql', 'Complete SELECT SQL query to execute against the Postgres data warehouse') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
16464,
5232
],
"id": "818bb6ad-6bb5-420e-9998-39123c65a3f0",
"name": "Export_File"
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MUS QuickBooks (realm 9341452327761715). ALWAYS include WHERE and MAXRESULTS.\n\nQBQL RULES: NEVER use IN(...), OR, nested parentheses, subqueries, JOINs, GROUP BY, aggregates.\nUse ORDERBY (not ORDER BY). Use MAXRESULTS (not LIMIT). Single quotes only.\nException: TxnDate date filters with = or >= and ISO date strings are valid.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
5552
],
"id": "d1aa28ce-a737-49a1-ac0f-f769972bd48f",
"name": "MUS - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Returns the single most recently created Invoice in MUS. Use to determine next sequential invoice number.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "SELECT DocNumber, TxnDate, MetaData FROM Invoice ORDERBY MetaData.CreateTime DESC MAXRESULTS 1"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
5552
],
"id": "b4989e6a-a3d0-44dd-acdd-e8bcc1defa4b",
"name": "MUS - Get latest invoice DocNumber",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Find MUS item(s) (products/services) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, Name, Description, UnitPrice, Type, Active FROM Item WHERE Name LIKE '%{{ $fromAI('name_fragment', 'Substring of the item name to search for', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
5552
],
"id": "1c447868-154d-44d9-8b68-87c90e945f91",
"name": "MUS - Find item by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"toolDescription": "Find MUS customer(s) by name fragment using QBQL LIKE. Returns up to 20 matches with Id, DisplayName, CompanyName, PrimaryEmailAddr.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, DisplayName, CompanyName, PrimaryEmailAddr, Active FROM Customer WHERE DisplayName LIKE '%{{ $fromAI('name_fragment', 'Substring of the customer name to search for', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
5552
],
"id": "0354e1d0-97ef-4a40-9968-18da85b07a92",
"name": "MUS - Find customer by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
}
},
{
"parameters": {
"jsCode": "const { customer_id, entity } = $input.item.json;\n\nif (!customer_id || !entity) {\n return [{ json: { error: 'customer_id and entity are required' } }];\n}\n\nconst company_id = entity;\n\nconst arResult = await $db.executeQuery(\n `SELECT\n COALESCE(SUM(balance), 0)::NUMERIC AS open_ar,\n COALESCE(MAX(CURRENT_DATE - duedate), 0) AS max_dpd,\n COUNT(*) FILTER (WHERE duedate < CURRENT_DATE) AS overdue_count\n FROM view_invoices_open\n WHERE customerref_name ILIKE $1\n AND company_id = $2`,\n [`%${customer_id}%`, company_id]\n);\n\nconst paymentResult = await $db.executeQuery(\n `SELECT COUNT(*) AS late_payments\n FROM view_payments p\n JOIN view_invoices_open i\n ON p.company_id = i.company_id\n AND p.\"CustomerRef_Name\" = i.customerref_name\n WHERE p.\"CustomerRef_Name\" ILIKE $1\n AND p.company_id = $2\n AND p.\"IsCustomerPayment\" = true\n AND p.\"TxnDate\" >= CURRENT_DATE - INTERVAL '6 months'\n AND p.\"TxnDate\" > i.duedate`,\n [`%${customer_id}%`, company_id]\n);\n\nconst row = arResult.rows[0];\nconst openAr = parseFloat(row.open_ar) || 0;\nconst maxDpd = parseInt(row.max_dpd) || 0;\nconst overdueCount = parseInt(row.overdue_count) || 0;\nconst latePayments = parseInt(paymentResult.rows[0].late_payments) || 0;\n\nlet score = 0;\nconst flags = [];\n\nif (maxDpd > 90) { score += 40; flags.push('90+ days past due'); }\nelse if (maxDpd > 60) { score += 25; flags.push('60–90 days past due'); }\nelse if (maxDpd > 30) { score += 10; flags.push('30–60 days past due'); }\n\nif (openAr > 100000) { score += 25; flags.push(`High open AR: $${openAr.toLocaleString()}`); }\nelse if (openAr > 50000) { score += 15; flags.push(`Elevated open AR: $${openAr.toLocaleString()}`); }\nelse if (openAr > 10000) { score += 5; flags.push(`Moderate open AR: $${openAr.toLocaleString()}`); }\n\nif (overdueCount > 5) { score += 20; flags.push(`${overdueCount} overdue invoices`); }\nelse if (overdueCount > 2) { score += 10; flags.push(`${overdueCount} overdue invoices`); }\nelse if (overdueCount > 0) { score += 5; flags.push(`${overdueCount} overdue invoice`); }\n\nif (latePayments > 5) { score += 15; flags.push(`${latePayments} late payments in last 6 months`); }\nelse if (latePayments > 2) { score += 8; flags.push(`${latePayments} late payments in last 6 months`); }\nelse if (latePayments > 0) { score += 3; flags.push(`${latePayments} late payment in last 6 months`); }\n\nconst rating = score >= 60 ? 'HIGH' : score >= 30 ? 'MEDIUM' : 'LOW';\n\nconst action =\n rating === 'HIGH' ? 'Escalate to finance leadership. Use formal tone in any outreach.' :\n rating === 'MEDIUM' ? 'Send collection reminder. Flag for follow-up in 5 business days.' :\n 'Standard collections cycle. Friendly reminder tone appropriate.';\n\nreturn [{ json: {\n customer_id,\n company_id,\n score,\n rating,\n flags,\n action,\n detail: {\n open_ar: openAr,\n max_dpd: maxDpd,\n overdue_invoices: overdueCount,\n late_payments_6mo: latePayments\n }\n}}];"
},
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.3,
"position": [
16240,
5232
],
"id": "f5d62a0f-c0e3-4391-a40b-24484be8a2bf",
"name": "score_customer_risk"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "H7My4glPDt56gXJz",
"mode": "list",
"cachedResultUrl": "/workflow/H7My4glPDt56gXJz",
"cachedResultName": "Collections Emails"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
16064,
5216
],
"id": "da32d790-a179-416b-83ff-0281932b67f4",
"name": "Call 'MoneyBot — Collections Emails'",
"disabled": true
},
{
"parameters": {
"name": "Generate_EML_File",
"description": "Generates a .eml email file and saves it to /files/emails/. Use when the user asks to create an email file, export an email, generate a .eml, or draft a payment reminder or invoice email as a downloadable file. Pass: to (recipient), from (sender), subject, body (plain text), cc (optional). Returns the saved file path.",
"workflowId": {
"__rl": true,
"value": "QWWFHJ57u8UvM11y",
"mode": "list",
"cachedResultUrl": "/workflow/QWWFHJ57u8UvM11y",
"cachedResultName": "Sub-Workflow EML generator"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"to": "={{ $fromAI('to', 'Recipient email address', 'string') }}",
"from": "={{ $fromAI('from', 'Sender email address', 'string') }}",
"subject": "={{ $fromAI('subject', 'Email subject line', 'string') }}",
"body": "={{ $fromAI('body', 'Plain text email body content', 'string') }}",
"cc": "={{ $fromAI('cc', 'Optional CC email address, empty string if none', 'string') }}"
},
"matchingColumns": [],
"schema": [
{
"id": "to",
"displayName": "to",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "from",
"displayName": "from",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "subject",
"displayName": "subject",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "body",
"displayName": "body",
"required": true,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "cc",
"displayName": "cc",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
}
},
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2,
"position": [
15872,
5232
],
"id": "0a54f690-895c-47ef-90c3-3c328a1429d5",
"name": "Generate EML File",
"disabled": true
},
{
"parameters": {
"toolDescription": "Fetch current exchange rates using exchangerate-api.com. Pass fromCurrency (e.g. CHF, USD) and the response contains rates to all major currencies. Use to convert MSAG/MHAG CHF amounts to USD for reporting.",
"url": "=https://api.exchangerate-api.com/v4/latest/{{ $fromAI('fromCurrency', 'Source currency code (e.g. CHF, USD, EUR)', 'string') || 'USD' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
},
{
"name": "User-Agent",
"value": "n8n-workflow"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
5232
],
"id": "69a1e4e4-3b8b-46e8-b42f-39d741760c07",
"name": "Get FX Rate"
},
{
"parameters": {
"operation": "create",
"displayName": "={{ $fromAI('Display_Name', `The customer's full name or company name`, 'string') }}",
"additionalFields": {}
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
16784,
6192
],
"id": "80640cac-e62e-40e7-aaf2-94408e5010f7",
"name": "MMX - Create customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"resource": "item",
"itemId": "={{ $fromAI('Item_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15936,
6352
],
"id": "692b658b-b4b5-4fae-ac94-d1494847fa84",
"name": "MMX - Get an item in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"customerId": "={{ $fromAI('Customer_ID', ``, 'string') }}"
},
"type": "n8n-nodes-base.quickbooksTool",
"typeVersion": 1,
"position": [
15680,
6352
],
"id": "3363a89a-35d6-41d8-948a-b6fe9294d8d2",
"name": "MMX - Get a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Run an arbitrary narrow QBQL SELECT against MMX QuickBooks (realm 9341452328017372). ALWAYS include WHERE and MAXRESULTS.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('qbql', 'A narrow QBQL SELECT statement. Must include WHERE and MAXRESULTS (<=100).', 'string') }}"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16464,
6192
],
"id": "ebb80d71-b6ed-430c-bea9-32d7b636e543",
"name": "MMX - Query QBO (QBQL)",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Returns the single most recently created Invoice in MMX. Use to determine next sequential invoice number.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "SELECT DocNumber, TxnDate, MetaData FROM Invoice ORDERBY MetaData.CreateTime DESC MAXRESULTS 1"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16192,
6192
],
"id": "636e1e14-dd99-4708-8bf8-c75a6197058f",
"name": "MMX - Get latest invoice DocNumber",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Find MMX item(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, Name, Description, UnitPrice, Type, Active FROM Item WHERE Name LIKE '%{{ $fromAI('name_fragment', 'Substring of the item name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15936,
6192
],
"id": "72157b53-e739-47ad-87b4-a41aba1c7e9e",
"name": "MMX - Find item by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"toolDescription": "Find MMX customer(s) by name fragment using QBQL LIKE. Returns up to 20 matches.",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "=SELECT Id, DisplayName, CompanyName, PrimaryEmailAddr, Active FROM Customer WHERE DisplayName LIKE '%{{ $fromAI('name_fragment', 'Substring of the customer name', 'string') }}%' MAXRESULTS 20"
},
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
15680,
6192
],
"id": "cb91a632-0272-4ac7-88d3-23a9e73159db",
"name": "MMX - Find customer by name",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
}
},
{
"parameters": {
"content": "# 🇺🇸 MUS — QuickBooks US (USD)\nRealm: 9341452327761715\n\nRead: Find customer/item, get DocNumber, ad-hoc QBQL\nWrite: Create/update customer · Create/send/update invoice\n\n> Always look up TaxCode before creating invoices.",
"height": 580,
"width": 1652,
"color": 2
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15632,
5440
],
"id": "c1bb76f7-c40e-48bc-a442-da54d7647010",
"name": "Sticky MUS"
},
{
"parameters": {
"content": "# Freshdesk",
"height": 544,
"width": 656,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
17344,
5488
],
"id": "7cea2fb8-749f-40db-9760-5e634ac9de96",
"name": "Sticky Note1"
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/contacts/autocomplete",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "term",
"value": "={{ $fromAI('contact_name', 'The name or partial name of the contact to search for. Examples: \"elsa peretti\", \"john\", \"smith\". Do not include field prefixes.', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
17408,
5648
],
"id": "a65faea6-3bef-4603-83c0-221173d303e2",
"name": "Freshdesk Contact Lookup - Finance",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Get many tickets in Freshdesk for Finance.\nWhen looking at open tickets, use status 2 and status 3 and check all batches.",
"operation": "getAll",
"returnAll": "=",
"options": {
"include": [
"description",
"company",
"stats",
"requester"
]
}
},
"type": "n8n-nodes-base.freshdeskTool",
"typeVersion": 1,
"position": [
17520,
5808
],
"id": "b8cb4668-edd7-477c-9aa6-5b158ec16678",
"name": "Freshdesk - finance tickets",
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Get many tickets in Freshdesk for customer support team.\nWhen looking at open tickets, use status 2 and status 3 and check all batches.",
"operation": "getAll",
"returnAll": "=",
"options": {
"include": [
"description",
"company",
"stats",
"requester"
]
}
},
"type": "n8n-nodes-base.freshdeskTool",
"typeVersion": 1,
"position": [
17632,
5648
],
"id": "99daa1c9-ebc7-4d89-85bb-357fc65681f8",
"name": "Freshdesk - general support",
"credentials": {
"freshdeskApi": {
"id": "eU5oyV62tEJdY6dV",
"name": "Freshdesk - general support"
}
}
},
{
"parameters": {
"authentication": "appToken",
"resource": "deal",
"operation": "getAll",
"returnAll": "=",
"filters": {}
},
"type": "n8n-nodes-base.hubspotTool",
"typeVersion": 2.2,
"position": [
17072,
5200
],
"id": "129384cf-27a0-46c6-aee3-c451c4506353",
"name": "Hubspot all deals search",
"credentials": {
"hubspotAppToken": {
"id": "DJFxH3uski1VveCl",
"name": "HubSpot account"
}
}
},
{
"parameters": {
"authentication": "appToken",
"resource": "company",
"operation": "getAll",
"returnAll": "=",
"options": {}
},
"type": "n8n-nodes-base.hubspotTool",
"typeVersion": 2.2,
"position": [
16896,
5200
],
"id": "c9e8afe7-9b53-4b70-8783-eb5f56842275",
"name": "Hubspot all companies search",
"credentials": {
"hubspotAppToken": {
"id": "DJFxH3uski1VveCl",
"name": "HubSpot account"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT\n id, excel_row_number, entity, deal_type, invoice_status,\n bill_freq, qb_item_type, close_date, bill_date, hs_customer,\n icon_id, qb_customer, qb_item_description, currency,\n amount_lcy, net_terms, tax, note\nFROM cust_fact_table\nWHERE\n UPPER(TRIM(invoice_status)) = 'UNBILLED'\n AND bill_date BETWEEN '{{ $fromAI('startDate', 'Start of bill date range in YYYY-MM-DD format', 'string') }}'::date\n AND '{{ $fromAI('endDate', 'End of bill date range in YYYY-MM-DD format', 'string') }}'::date\n AND entity IS NOT NULL\n AND amount_lcy IS NOT NULL\n AND amount_lcy <> 0\n AND COALESCE(run_status, '') <> 'created'\n AND COALESCE(on_hold, false) = false\nORDER BY entity, bill_date",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
17280,
5200
],
"id": "71698190-a071-4b15-b5c7-2bad605c033f",
"name": "Read Customer Fact Table for Billing",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"toolDescription": "Update a MUS customer (realm 9341452327761715). Requires Customer_ID and SyncToken from a prior query. Sparse update — only fields passed are changed. Supports: CompanyName, FullyQualifiedName, PrimaryEmailAddr, PrimaryPhone, Notes, BillAddr fields.\n\nMultiple email contacts can be added to PrimaryEmailAddr using comma separation i.e. 1@abc.com, 2@abc.com, 3@abc.com",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/customer?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('Customer_ID', 'The QuickBooks ID of the customer to update', 'string'), SyncToken: $fromAI('SyncToken', 'SyncToken from a prior query — required for updates', 'string'), sparse: true }; const companyName = $fromAI('Company_Name', 'Optional new company name, empty to skip', 'string'); const fqn = $fromAI('Fully_Qualified_Name', 'Optional new fully qualified name, empty to skip', 'string'); const email = $fromAI('Primary_Email_Address', 'Optional new primary email, empty to skip', 'string'); const phone = $fromAI('Primary_Phone', 'Optional new primary phone, empty to skip', 'string'); const notes = $fromAI('Notes', 'Optional customer note (appears on profile and invoices, max 4000 chars), empty to skip', 'string'); const line1 = $fromAI('Line_1', 'Optional billing address line 1, empty to skip', 'string'); const city = $fromAI('City', 'Optional billing address city, empty to skip', 'string'); const postalCode = $fromAI('Postal_Code', 'Optional billing address postal code, empty to skip', 'string'); const state = $fromAI('Country_Subdivision_Code', 'Optional state/province code, empty to skip', 'string'); if (companyName && companyName.trim()) p.CompanyName = companyName.trim(); if (fqn && fqn.trim()) p.FullyQualifiedName = fqn.trim(); if (email && email.trim()) p.PrimaryEmailAddr = { Address: email.trim() }; if (phone && phone.trim()) p.PrimaryPhone = { FreeFormNumber: phone.trim() }; if (notes && notes.trim()) p.Notes = notes.trim(); if (line1 || city || postalCode || state) { p.BillAddr = {}; if (line1 && line1.trim()) p.BillAddr.Line1 = line1.trim(); if (city && city.trim()) p.BillAddr.City = city.trim(); if (postalCode && postalCode.trim()) p.BillAddr.PostalCode = postalCode.trim(); if (state && state.trim()) p.BillAddr.CountrySubDivisionCode = state.trim(); } return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17088,
5568
],
"id": "1f695e1a-d0a9-45a9-9d57-9e9049b7e13d",
"name": "MUS - Update a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Update a MMX customer (realm 9341452328017372). Requires Customer_ID and SyncToken from a prior query. Sparse update — only fields passed are changed. Supports: CompanyName, FullyQualifiedName, PrimaryEmailAddr, PrimaryPhone, Notes, BillAddr fields.\n\nMultiple email contacts can be added to PrimaryEmailAddr using comma separation i.e. 1@abc.com, 2@abc.com, 3@abc.com",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/customer?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('Customer_ID', 'The QuickBooks ID of the customer to update', 'string'), SyncToken: $fromAI('SyncToken', 'SyncToken from a prior query — required for updates', 'string'), sparse: true }; const companyName = $fromAI('Company_Name', 'Optional new company name, empty to skip', 'string'); const fqn = $fromAI('Fully_Qualified_Name', 'Optional new fully qualified name, empty to skip', 'string'); const email = $fromAI('Primary_Email_Address', 'Optional new primary email, empty to skip', 'string'); const phone = $fromAI('Primary_Phone', 'Optional new primary phone, empty to skip', 'string'); const notes = $fromAI('Notes', 'Optional customer note (appears on profile and invoices, max 4000 chars), empty to skip', 'string'); const line1 = $fromAI('Line_1', 'Optional billing address line 1, empty to skip', 'string'); const city = $fromAI('City', 'Optional billing address city, empty to skip', 'string'); const postalCode = $fromAI('Postal_Code', 'Optional billing address postal code, empty to skip', 'string'); const state = $fromAI('Country_Subdivision_Code', 'Optional state/province code, empty to skip', 'string'); if (companyName && companyName.trim()) p.CompanyName = companyName.trim(); if (fqn && fqn.trim()) p.FullyQualifiedName = fqn.trim(); if (email && email.trim()) p.PrimaryEmailAddr = { Address: email.trim() }; if (phone && phone.trim()) p.PrimaryPhone = { FreeFormNumber: phone.trim() }; if (notes && notes.trim()) p.Notes = notes.trim(); if (line1 || city || postalCode || state) { p.BillAddr = {}; if (line1 && line1.trim()) p.BillAddr.Line1 = line1.trim(); if (city && city.trim()) p.BillAddr.City = city.trim(); if (postalCode && postalCode.trim()) p.BillAddr.PostalCode = postalCode.trim(); if (state && state.trim()) p.BillAddr.CountrySubDivisionCode = state.trim(); } return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17072,
6192
],
"id": "8e541a9d-1003-4e4d-9d7f-02083fcceac3",
"name": "MMX - Update a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Update a MSAG customer (realm 9341452373926753). Requires Customer_ID and SyncToken from a prior query. Sparse update — only fields passed are changed. Supports: CompanyName, FullyQualifiedName, PrimaryEmailAddr, PrimaryPhone, Notes, BillAddr fields.\n\nMultiple email contacts can be added to PrimaryEmailAddr using comma separation i.e. 1@abc.com, 2@abc.com, 3@abc.com",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/customer?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { Id: $fromAI('Customer_ID', 'The QuickBooks ID of the customer to update', 'string'), SyncToken: $fromAI('SyncToken', 'SyncToken from a prior query — required for updates', 'string'), sparse: true }; const companyName = $fromAI('Company_Name', 'Optional new company name, empty to skip', 'string'); const fqn = $fromAI('Fully_Qualified_Name', 'Optional new fully qualified name, empty to skip', 'string'); const email = $fromAI('Primary_Email_Address', 'Optional new primary email, empty to skip', 'string'); const phone = $fromAI('Primary_Phone', 'Optional new primary phone, empty to skip', 'string'); const notes = $fromAI('Notes', 'Optional customer note (appears on profile and invoices, max 4000 chars), empty to skip', 'string'); const line1 = $fromAI('Line_1', 'Optional billing address line 1, empty to skip', 'string'); const city = $fromAI('City', 'Optional billing address city, empty to skip', 'string'); const postalCode = $fromAI('Postal_Code', 'Optional billing address postal code, empty to skip', 'string'); const state = $fromAI('Country_Subdivision_Code', 'Optional state/province code, empty to skip', 'string'); if (companyName && companyName.trim()) p.CompanyName = companyName.trim(); if (fqn && fqn.trim()) p.FullyQualifiedName = fqn.trim(); if (email && email.trim()) p.PrimaryEmailAddr = { Address: email.trim() }; if (phone && phone.trim()) p.PrimaryPhone = { FreeFormNumber: phone.trim() }; if (notes && notes.trim()) p.Notes = notes.trim(); if (line1 || city || postalCode || state) { p.BillAddr = {}; if (line1 && line1.trim()) p.BillAddr.Line1 = line1.trim(); if (city && city.trim()) p.BillAddr.City = city.trim(); if (postalCode && postalCode.trim()) p.BillAddr.PostalCode = postalCode.trim(); if (state && state.trim()) p.BillAddr.CountrySubDivisionCode = state.trim(); } return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17104,
6800
],
"id": "74d73667-2416-4e71-a9e7-cb8c9656404b",
"name": "MSAG - Update a customer in QuickBooks Online",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Update a Freshdesk Finance ticket. Use to change status, priority, or subject. Status codes: 2=Open, 3=Pending, 4=Resolved, 5=Closed. Always confirm before closing or resolving. Requires numeric ticket ID from a prior lookup.",
"method": "PUT",
"url": "=https://masttro.freshdesk.com/api/v2/tickets/{{ $fromAI('ticket_id', 'Numeric Freshdesk ticket ID', 'string') }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = {}; const status = $fromAI('status', 'Ticket status: 2=Open, 3=Pending, 4=Resolved, 5=Closed. Pass 0 to skip.', 'number'); const priority = $fromAI('priority', 'Priority: 1=Low, 2=Medium, 3=High, 4=Urgent. Pass 0 to skip.', 'number'); const subject = $fromAI('subject', 'Optional new subject. Empty string to skip.', 'string'); if (status && status > 0) p.status = status; if (priority && priority > 0) p.priority = priority; if (subject && subject.trim()) p.subject = subject.trim(); return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17696,
5824
],
"id": "dfe291b7-c3c5-41a6-9916-f83200b97b8d",
"name": "Freshdesk - Update Ticket",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Add a note to a Freshdesk Finance ticket. Private notes are internal only (default). Public notes are visible to the requester. Always retrieve ticket ID via search before calling.",
"method": "POST",
"url": "=https://masttro.freshdesk.com/api/v2/tickets/{{ $fromAI('ticket_id', 'Numeric Freshdesk ticket ID', 'string') }}/notes",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ body: $fromAI('body', 'Note content — plain text or HTML', 'string'), private: $fromAI('private', 'true for internal note, false for customer-visible', 'boolean') ?? true }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17840,
5616
],
"id": "8c7fabaa-a457-411e-b569-b24c0ae88fa6",
"name": "Freshdesk - Add Note to Ticket",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
},
"disabled": true
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "pdf-mime-check",
"leftValue": "={{ $json.files?.[0]?.mimeType }}",
"rightValue": "application/pdf",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
15792,
4768
],
"id": "7acb2049-f340-43e8-a691-8feccc8be128",
"name": "PDF upload?"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "merge-chatinput",
"name": "chatInput",
"value": "={{ $('When chat message received').item.json.chatInput }}\n\n--- Uploaded Document ---\n{{ $json.text }}",
"type": "string"
},
{
"id": "pass-session",
"name": "sessionId",
"value": "={{ $('When chat message received').item.json.sessionId }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
16176,
4496
],
"id": "4b87f084-42ce-4f0b-9e60-c0343d6419fa",
"name": "Merge PDF Text"
},
{
"parameters": {
"operation": "pdf",
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
"options": {
"joinPages": true
}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1,
"position": [
16000,
4512
],
"id": "2b54f2f0-d4ec-4ff2-9f26-bdd437b361d4",
"name": "Extract from File"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "excel-mime-check",
"leftValue": "={{ $json.files?.[0]?.mimeType }}",
"rightValue": "vnd\\.ms-excel|spreadsheetml",
"operator": {
"type": "string",
"operation": "regex"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
15968,
4768
],
"id": "ec0ff3a8-459a-47fd-9a0c-307aa8e90167",
"name": "Excel upload?"
},
{
"parameters": {
"operation": "xlsx",
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
"options": {}
},
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1.1,
"position": [
16128,
4752
],
"id": "c683404a-97b6-4f03-a6f5-827536a28097",
"name": "Extract from File (XLSX)"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "merge-chatinput-xlsx",
"name": "chatInput",
"value": "={{ $('When chat message received').item.json.chatInput }}\n\n--- Uploaded Spreadsheet ---\n{{ JSON.stringify($json, null, 2) }}",
"type": "string"
},
{
"id": "pass-session-xlsx",
"name": "sessionId",
"value": "={{ $('When chat message received').item.json.sessionId }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
16304,
4640
],
"id": "5db4a7bc-0f54-48a0-8fed-4a64860d0efa",
"name": "Merge XLSX Data"
},
{
"parameters": {
"toolDescription": "Create a new Freshdesk Finance ticket. Use when the user asks to create, open, or log a new ticket — NOT when adding to an existing one. Required: subject and description. Optional: priority (1=Low, 2=Medium, 3=High, 4=Urgent), status (2=Open default), email (requester email — omit if unknown), cc_emails (array of strings).\\n\\nAlways post description as HTML for good formatting.\\nDo NOT use this to update or comment on existing tickets — use Freshdesk - Update Ticket or Freshdesk - Add Note to Ticket instead.",
"method": "POST",
"url": "=https://masttro.freshdesk.com/api/v2/tickets/",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => { const p = { subject: $fromAI('subject', 'Ticket subject line', 'string'), description: $fromAI('description', 'Ticket body — HTML preferred', 'string'), status: $fromAI('status', '2=Open, 3=Pending', 'number') || 2, priority: $fromAI('priority', '1=Low, 2=Medium, 3=High, 4=Urgent', 'number') || 2 }; const email = $fromAI('email', 'Requester email. Empty string for internal tickets.', 'string'); p.email = (email && email.trim()) ? email.trim() : 'mnolan@masttro.com'; return JSON.stringify(p); })() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17856,
5808
],
"id": "6525ca16-86a3-4b47-a5d1-85db4142157e",
"name": "Freshdesk - Create Ticket",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Download a QBO invoice as a PDF and return a clickable download link. Works for MUS, MMX, and MSAG. Pass entity (MUS, MMX, or MSAG) and invoiceId (QBO numeric Id, not DocNumber). Optionally pass a filename. After calling, return the message field verbatim — it contains the clickable download link.",
"method": "POST",
"url": "http://10.116.0.2:5679/webhook/invoice-pdf",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "entity",
"value": "={{ $fromAI('entity', 'Entity code: MUS, MMX, or MSAG', 'string') }}"
},
{
"name": "invoiceId",
"value": "={{ $fromAI('invoiceId', 'QBO numeric Invoice Id (not DocNumber)', 'string') }}"
},
{
"name": "filename",
"value": "={{ $fromAI('filename', 'Optional filename e.g. invoice_MUS_1234.pdf — empty string for auto', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16704,
5216
],
"id": "58a640c1-a7ff-4c5e-8ff8-c84180fc81e8",
"name": "Download Invoice PDF"
},
{
"parameters": {
"toolDescription": "Create a credit memo in MUS (realm 9341452327761715, USD). Use when issuing a credit, refund, or adjustment against a customer account — NOT an invoice.\n\nWorkflow:\n 1. Resolve customerId via MUS - Find customer by name\n 2. Resolve itemId via MUS - Find item by name\n 3. Get next DocNumber: MUS - Get latest invoice DocNumber → +1 → collision-check CreditMemo + Estimate\n 4. Look up taxCodeId: TX or NY state → 'TAX', all others → 'NON'\n 5. Confirm details with user before posting\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, TotalAmt, TxnDate, CustomerRef name. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const TAXABLE_STATES = ['TX', 'NY'];\n const customerState = $fromAI('customerState', 'Two-letter US state code from customer BillAddr.CountrySubDivisionCode. Empty string if unknown.', 'string');\n const stateTaxCode = TAXABLE_STATES.includes((customerState || '').toUpperCase().trim()) ? 'TAX' : 'NON';\n const resolveTaxCode = (id) => { const map = { 'TAX': 'TAX', 'NON': 'NON', '1': 'TAX', '2': 'NON' }; return map[String(id)] || stateTaxCode; };\n const linesRaw = $fromAI('lines', 'JSON array of line items. Each: amount, itemId, description, qty, unitPrice, taxCodeId.', 'string');\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n const p = {\n DocNumber: $fromAI('docNumber', 'Next sequential DocNumber as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Credit memo date YYYY-MM-DD', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'QBO numeric customer Id', 'string') },\n Line: lines.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: resolveTaxCode(l.taxCodeId) }\n }\n }))\n };\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim();\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17168,
5840
],
"id": "2b254fe5-6224-466b-80df-a506beea942d",
"name": "MUS - Create Credit Memo",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a credit memo in MMX (realm 9341452328017372, USD). Use when issuing a credit, refund, or adjustment against a customer account — NOT an invoice.\n\nWorkflow:\n 1. Resolve customerId via MMX - Find customer by name\n 2. Resolve itemId via MMX - Find item by name\n 3. Get next DocNumber: MMX - Get latest invoice DocNumber → +1 → collision-check CreditMemo + Estimate\n 4. MMX customers are non-US — always pass taxCodeId = 'NON'\n 5. Confirm details with user before posting\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, TotalAmt, TxnDate, CustomerRef name. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const linesRaw = $fromAI('lines', 'JSON array of line items. Each: amount, itemId, description, qty, unitPrice. taxCodeId always NON for MMX.', 'string');\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n const p = {\n DocNumber: $fromAI('docNumber', 'Next sequential DocNumber as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Credit memo date YYYY-MM-DD', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'QBO numeric customer Id', 'string') },\n Line: lines.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: 'NON' }\n }\n }))\n };\n const c = $fromAI('currency', 'ISO currency code. Empty string for entity default (USD).', 'string');\n if (c && c.trim()) p.CurrencyRef = { value: c.trim().toUpperCase() };\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim();\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17152,
6368
],
"id": "a61dde4f-5a00-491a-b3b3-3161b3adf864",
"name": "MMX - Create Credit Memo",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Create a credit memo in MSAG (realm 9341452373926753, CHF). Use when issuing a credit, refund, or adjustment against a customer account — NOT an invoice.\n\nWorkflow:\n 1. Resolve customerId via MSAG - Find customer by name\n 2. Resolve itemId via MSAG - Find item by name\n 3. Get next DocNumber: MSAG - Get latest invoice DocNumber → +1 → collision-check CreditMemo + Estimate\n 4. Query TaxCode (SELECT Id, Name FROM TaxCode WHERE Active = true MAXRESULTS 50) to resolve Swiss VAT code — never hardcode\n 5. Confirm details with user before posting\n 6. After posting, call Get FX Rate (fromCurrency=CHF) to convert amount to USD for reporting\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, TotalAmt, TxnDate, CustomerRef name. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ (() => {\n const linesRaw = $fromAI('lines', 'JSON array of line items. Each: amount, itemId, description, qty, unitPrice, taxCodeId (QBO TaxCode Id from TaxCode query — never null or hardcoded).', 'string');\n const lines = typeof linesRaw === 'string' ? JSON.parse(linesRaw) : linesRaw;\n const p = {\n DocNumber: $fromAI('docNumber', 'Next sequential DocNumber as a string', 'string'),\n TxnDate: $fromAI('txnDate', 'Credit memo date YYYY-MM-DD', 'string'),\n CustomerRef: { value: $fromAI('customerId', 'QBO numeric customer Id', 'string') },\n Line: lines.map(l => ({\n Amount: Number(l.amount),\n DetailType: 'SalesItemLineDetail',\n Description: l.description,\n SalesItemLineDetail: {\n ItemRef: { value: String(l.itemId) },\n Qty: l.qty ?? 1,\n UnitPrice: Number(l.unitPrice),\n TaxCodeRef: { value: (l.taxCodeId && String(l.taxCodeId).trim()) ? String(l.taxCodeId).trim() : 'NON' }\n }\n }))\n };\n const privateNote = $fromAI('privateNote', 'Optional memo. Empty string to omit.', 'string');\n if (privateNote && privateNote.trim()) p.PrivateNote = privateNote.trim();\n return JSON.stringify(p);\n})() }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
17136,
7120
],
"id": "0749b243-fada-44d0-a213-fda59f446ea3",
"name": "MSAG - Create Credit Memo",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"contextWindowLength": 10
},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.4,
"position": [
16832,
4928
],
"id": "05180f32-ec76-4a12-9549-e40906cb9e80",
"name": "Simple Memory"
},
{
"parameters": {
"toolDescription": "Apply a credit memo to an open invoice in MUS (realm 9341452327761715). Use when a credit memo exists and needs to be matched against an outstanding invoice — resulting in a $0 payment that closes both.\n\nWorkflow (all steps required):\n 1. Retrieve Credit Memo Id and SyncToken via MUS - Query QBO (QBQL):\n SELECT Id, SyncToken, DocNumber, TotalAmt, RemainingCredit FROM CreditMemo WHERE Id = '<id>' MAXRESULTS 1\n 2. Retrieve the Invoice Id via Postgres view_invoices_open or MUS - Query QBO (QBQL)\n 3. Present credit memo and invoice details to user and confirm before applying\n 4. Call this tool with creditMemoId, syncToken, and invoiceId\n\nThis posts a sparse update to the credit memo with a LinkedTxn pointing to the invoice. QBO will net the credit against the invoice balance automatically.\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, RemainingCredit, and the linked Invoice Id. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452327761715/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('creditMemoId', 'The QBO numeric Credit Memo Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from a prior QBQL query — required for updates', 'string'), sparse: true, Line: [{ Amount: $fromAI('amount', 'Amount to apply — must equal the lesser of the credit memo RemainingCredit and the invoice balance', 'number'), LinkedTxn: [{ TxnId: $fromAI('invoiceId', 'The QBO numeric Invoice Id to apply the credit memo against', 'string'), TxnType: 'Invoice' }] }] }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16912,
5488
],
"id": "0136e7bc-e3bc-42cd-a20c-b23c2a26b6dc",
"name": "MUS - Apply Credit Memo to Invoice",
"credentials": {
"quickBooksOAuth2Api": {
"id": "i6dFL9yjAAhFxsmn",
"name": "QuickBooks Online MUS"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Apply a credit memo to an open invoice in MMX (realm 9341452328017372). Use when a credit memo exists and needs to be matched against an outstanding invoice — resulting in a $0 payment that closes both.\n\nWorkflow (all steps required):\n 1. Retrieve Credit Memo Id and SyncToken via MMX - Query QBO (QBQL):\n SELECT Id, SyncToken, DocNumber, TotalAmt, RemainingCredit FROM CreditMemo WHERE Id = '<id>' MAXRESULTS 1\n 2. Retrieve the Invoice Id via Postgres view_invoices_open or MMX - Query QBO (QBQL)\n 3. Present credit memo and invoice details to user and confirm before applying\n 4. Call this tool with creditMemoId, syncToken, and invoiceId\n\nThis posts a sparse update to the credit memo with a LinkedTxn pointing to the invoice. QBO will net the credit against the invoice balance automatically.\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, RemainingCredit, and the linked Invoice Id. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452328017372/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('creditMemoId', 'The QBO numeric Credit Memo Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from a prior QBQL query — required for updates', 'string'), sparse: true, Line: [{ Amount: $fromAI('amount', 'Amount to apply — must equal the lesser of the credit memo RemainingCredit and the invoice balance', 'number'), LinkedTxn: [{ TxnId: $fromAI('invoiceId', 'The QBO numeric Invoice Id to apply the credit memo against', 'string'), TxnType: 'Invoice' }] }] }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16944,
6112
],
"id": "ca1e597e-1b04-4202-9fdd-be57905e7f57",
"name": "MMX - Apply Credit Memo to Invoice",
"credentials": {
"quickBooksOAuth2Api": {
"id": "USLCXCphdJqcpr2C",
"name": "QuickBooks Online MMX"
}
},
"disabled": true
},
{
"parameters": {
"toolDescription": "Apply a credit memo to an open invoice in MSAG (realm 9341452373926753). Use when a credit memo exists and needs to be matched against an outstanding invoice — resulting in a CHF 0 payment that closes both.\n\nWorkflow (all steps required):\n 1. Retrieve Credit Memo Id and SyncToken via MSAG - Query QBO (QBQL):\n SELECT Id, SyncToken, DocNumber, TotalAmt, RemainingCredit FROM CreditMemo WHERE Id = '<id>' MAXRESULTS 1\n 2. Retrieve the Invoice Id via Postgres view_invoices_open or MSAG - Query QBO (QBQL)\n 3. Present credit memo and invoice details to user and confirm before applying\n 4. Call this tool with creditMemoId, syncToken, and invoiceId\n\nThis posts a sparse update to the credit memo with a LinkedTxn pointing to the invoice. QBO will net the credit against the invoice balance automatically.\n\nAfter a successful POST (CreditMemo.Id present in response), return only: CreditMemo.Id, DocNumber, RemainingCredit, and the linked Invoice Id. Then call Get FX Rate (fromCurrency=CHF) to convert any amounts to USD for reporting. Do not surface raw response JSON.",
"method": "POST",
"url": "https://quickbooks.api.intuit.com/v3/company/9341452373926753/creditmemo?minorversion=65",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({ Id: $fromAI('creditMemoId', 'The QBO numeric Credit Memo Id — retrieve via QBQL first', 'string'), SyncToken: $fromAI('syncToken', 'SyncToken from a prior QBQL query — required for updates', 'string'), sparse: true, Line: [{ Amount: $fromAI('amount', 'Amount to apply — must equal the lesser of the credit memo RemainingCredit and the invoice balance', 'number'), LinkedTxn: [{ TxnId: $fromAI('invoiceId', 'The QBO numeric Invoice Id to apply the credit memo against', 'string'), TxnType: 'Invoice' }] }] }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.2,
"position": [
16960,
7136
],
"id": "13fdf991-1331-4e62-8a17-6bd74143be8e",
"name": "MSAG - Apply Credit Memo to Invoice",
"credentials": {
"quickBooksOAuth2Api": {
"id": "Q0bvGHliYfQTGUWW",
"name": "QuickBooks Online MSAG"
}
},
"disabled": true
},
{
"parameters": {
"content": "# Microsoft Teams",
"height": 448,
"width": 768
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
17504,
4640
],
"id": "03baead9-ff4f-47bb-8dd8-48020cfa5834",
"name": "Sticky Note2"
},
{
"parameters": {
"public": true,
"initialMessages": "💰 MoneyBot (READ-ONLY) online.\nI can check QBO customers, find real time FX rates, query Hubspot deals (closed/won), risk score customers with open invoices, create CSV files and query 1,200+ customer contracts and amendments in memory. I can export invoices to PDF.\n\nYou can upload images, XLSX or PDFs for review.",
"availableInChat": true,
"agentName": "Moneybot",
"options": {
"allowFileUploads": true,
"allowedFilesMimeTypes": "*"
}
},
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1.4,
"position": [
15632,
4512
],
"id": "2d461a9b-bb76-44a1-8f19-82bb3d584a84",
"name": "When chat message received",
"webhookId": "moneybot-readonly",
"alwaysOutputData": true
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-5",
"mode": "list",
"cachedResultName": "Claude Sonnet 5"
},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
16848,
4480
],
"id": "884af69f-cde9-4acd-9191-86e34ca41f3e",
"name": "Claude",
"credentials": {
"anthropicApi": {
"id": "uJ4J74wLCEWqVd9B",
"name": "Anthropic - Moneybot"
}
}
},
{
"parameters": {
"model": "gemma4:12b",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [
17040,
4480
],
"id": "071ce6d1-5379-4964-8d10-36682cceadc9",
"name": "Ollama",
"credentials": {
"ollamaApi": {
"id": "PjA3zj5FSQY841F6",
"name": "Ollama 100.111.46.13 fireball"
}
}
}
],
"connections": {
"MMX - Create Invoice via QuickBooks API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Send Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Update Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Find customer by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Find item by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Get latest invoice DocNumber": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Get a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Get an item in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Invoice via QuickBooks API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Send Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Update Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks open invoices": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks customer payments": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks general ledger": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot deals": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MHAG - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MHAG - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MHAG - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MGC - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MGC - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MGC - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres PGVector Store": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"mistral-embed": {
"ai_embedding": [
[
{
"node": "Postgres PGVector Store",
"type": "ai_embedding",
"index": 0
}
]
]
},
"Contract_Direct_Lookup": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Quickbooks Chart of Accounts MGC MUS MMX MSAG MHAG": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams DM Chats — returns all 1:1 chats with id and members. Search members for displayName or email matching the target person. Use chatId with Send Teams DM.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams Group Chats — returns all group chats with id, topic, and members. Match by topic name or member displayName. Use chatId with Send Teams DM.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams List — returns all joined teams with id and displayName. Call this first to resolve a team name to a teamId before calling Get Teams Channels.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get Teams Channels — given a teamId from Get Teams List, returns all channels with id and displayName. Use channelId and teamId together with Send Teams Channel Message.": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Send Teams DM": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Send Teams Channel Message": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Postgres Quickbooks balance sheet": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Void Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Void Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Void Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Customer Payment": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Get a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Get an item in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Invoice via QuickBooks API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Send Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Update Invoice via API": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - delete Journal Entry": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Export_File": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Get latest invoice DocNumber": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Find item by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Find customer by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"score_customer_risk": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Call 'MoneyBot — Collections Emails'": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Generate EML File": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Get FX Rate": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Get an item in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Get a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Query QBO (QBQL)": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Get latest invoice DocNumber": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Find item by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Find customer by name": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk Contact Lookup - Finance": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - finance tickets": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - general support": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot all deals search": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot all companies search": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Read Customer Fact Table for Billing": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Update a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Update a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Update a customer in QuickBooks Online": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - Update Ticket": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - Add Note to Ticket": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"PDF upload?": {
"main": [
[
{
"node": "Extract from File",
"type": "main",
"index": 0
}
],
[
{
"node": "Excel upload?",
"type": "main",
"index": 0
}
]
]
},
"Merge PDF Text": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Extract from File": {
"main": [
[
{
"node": "Merge PDF Text",
"type": "main",
"index": 0
}
]
]
},
"Excel upload?": {
"main": [
[
{
"node": "Extract from File (XLSX)",
"type": "main",
"index": 0
}
],
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Extract from File (XLSX)": {
"main": [
[
{
"node": "Merge XLSX Data",
"type": "main",
"index": 0
}
]
]
},
"Merge XLSX Data": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Freshdesk - Create Ticket": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Download Invoice PDF": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MUS - Create Credit Memo": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Create Credit Memo": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Create Credit Memo": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Simple Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"MUS - Apply Credit Memo to Invoice": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MMX - Apply Credit Memo to Invoice": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"MSAG - Apply Credit Memo to Invoice": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"When chat message received": {
"main": [
[
{
"node": "PDF upload?",
"type": "main",
"index": 0
}
]
]
},
"Claude": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
}
},
"nodeGroups": [],
"authors": "Michael Nolan",
"name": "Version dd228b98",
"description": "",
"autosaved": true
},
"tags": []
}