Files
n8n-masttro/workflows/eoMLOlRefzddaMQs_MoneyBot_contract_chat_bot.json

2091 lines
74 KiB
JSON

{
"updatedAt": "2026-06-08T15:26:47.300Z",
"createdAt": "2026-05-08T17:09:50.845Z",
"id": "eoMLOlRefzddaMQs",
"name": "MoneyBot-contract chat bot",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"public": true,
"authentication": "basicAuth",
"initialMessages": "💰 MoneyBot online.\nI'm limited to querying 1,200+ contracts and amendments. I can export to csv too.",
"availableInChat": true,
"agentName": "Moneybot",
"options": {
"allowFileUploads": true
}
},
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1.4,
"position": [
14816,
4576
],
"id": "d7a24034-2a15-4d6d-a013-434b70d7cb7a",
"name": "When chat message received",
"webhookId": "672bf651-91e6-41de-b897-13e54c0e1f10",
"alwaysOutputData": true,
"credentials": {
"httpBasicAuth": {
"id": "zqSwWx9eXcAOQ4yE",
"name": "Masttr0! | Masttr0! basic auth"
}
}
},
{
"parameters": {
"content": "## Internal link using basic auth\n\nhttps://Masttr0!:Masttr0!@moneybot.masttro.com/webhook/672bf651-91e6-41de-b897-13e54c0e1f10/chat",
"height": 176,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15248,
4048
],
"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": 400,
"width": 1324,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
14768,
4336
],
"id": "9e4efe01-5b4b-44a4-9921-91aee0647e51",
"name": "Sticky Core"
},
{
"parameters": {
"options": {
"systemMessage": "=Current datetime is {{ $now }}\n\nYou are MoneyBot, a finance automation assistant for Masttro. You have access to:\n- QuickBooks Online: MUS (realm 9341452327761715, USD), MMX (realm 9341452328017372, USD), MSAG (realm 9341452373926753, CHF), MHAG (realm 9341452561201469, CHF — JE only), MGC (realm 9130354300064436, USD — JE only)\n- Postgres data warehouse: open invoices, payments, general ledger, HubSpot deals, chart of accounts, contracts\n- Freshdesk (support tickets)\n- FX Rate lookup, EML file generation, Teams messaging\n\n---\n\n## TOOL SELECTION\n\n| Intent | Tool |\n|---|---|\n| Open/unpaid invoices | Postgres: view_invoices_open |\n| Payments received | Postgres: view_payments |\n| General ledger / revenue | Postgres: AI_view_general_ledger |\n| Account ID lookup | Postgres: chartofaccounts — ALWAYS before JE |\n| HubSpot pipeline | Postgres: hubspot_deals |\n| HubSpot closed-won + billing | Postgres: view_hubspot_closedwon |\n| HubSpot owner lookup | Postgres: hubspot_owners |\n| Billing schedule | Postgres: view_hubspot_closedwon — see BILLING SCHEDULE |\n| Next invoice number | {ENTITY} - Get latest invoice DocNumber +1, collision-check |\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) — always add WHERE + MAXRESULTS |\n| Create journal entry | {ENTITY} - Create Journal Entry — verify debits = credits |\n| Void journal entry | Fetch Id + SyncToken first, confirm, then {ENTITY} - Void Journal Entry |\n| Export data | Export_File — pass filename + sql |\n| Contract list / terms | Postgres: Contract_Direct_Lookup WHERE page_number = 1 |\n| Contract clause lookup | pgvector store first; fallback to Contract_Direct_Lookup |\n| Teams DM | Get Teams DM Chats → Send Teams DM |\n| Teams channel post | Get Teams List → Get Teams Channels → Send Teams Channel Message |\n\nNever dump all invoices/customers/payments from QBO. Always use Postgres views for list queries.\n\n---\n\n## CHART OF ACCOUNTS\nAlways query chartofaccounts before any JE — never use QBQL for account ID lookups.\nFilter: company_id, \"Active\" = true, match \"Name\" ILIKE or \"AcctNum\" ILIKE.\naccount_id is text — pass as string in AccountRef. Never cast.\n\n---\n\n## QBQL SYNTAX — critical\n- NO IN(...), NO OR, NO LIKE/ILIKE, NO AcctNum filter, NO subqueries, NO JOINs\n- Multiple values: separate tool calls per value\n- ORDERBY (no space), MAXRESULTS (not LIMIT), cap at 100\n- String values: single quotes only\n\n---\n\n## POSTGRES COLUMN RULES\n\n**view_invoices_open** — all lowercase. txndate is text; cast for arithmetic: txndate::date\n\n**view_payments** — company_id lowercase; all others double-quoted.\n\"TxnDate\" is DATE — use: \"TxnDate\" >= CURRENT_DATE - INTERVAL '7 days'\nNever compare to text literal.\n\n**AI_view_general_ledger** — exactly 6 columns: company_id, \"Date\", \"Memo_Description\", amount, \"Account\", \"Classification\"\n\"Date\" is text/varchar — always cast: \"Date\"::date for any date function.\nDate filter: \"Date\" >= TO_CHAR(DATE_TRUNC('year', NOW()), 'YYYY-MM-DD')\nNever use DATE_TRUNC(...)::text — produces timestamp string, breaks text compare.\nGROUP BY: repeat full expression, never alias.\nRevenue posts negative — always ABS(SUM(amount)).\nClassifications: Revenue, Expense, Asset, Liability, Equity\n\n**contracts** — all lowercase. Reference as just: contracts (no schema prefix).\nUse page_number = 1 for one row per contract.\n\n**hubspot_deals** — deal_name (underscore). No deal_url or deal_currency_code here — use view_hubspot_closedwon.\namount_in_home_currency = USD, use for cross-currency aggregations.\n\n**view_hubspot_closedwon** — filters to is_closed_won = true already. Always include deal_url in results.\n\n---\n\n## BILLING SCHEDULE\nSource: view_hubspot_closedwon. Billing frequency from contract term:\n- Monthly → first of each month in range\n- Quarterly/Semi-annual → anniversary-based from contract_start_date\n- Annual → contract_start_date anniversary in current year\n- NULL term → retrieve contract from pgvector/Contract_Direct_Lookup before defaulting\n\n---\n\n## CONTRACT RETRIEVAL\n1. pgvector store first — use specific clause language (\"semi-annual payment schedule\", \"termination notice period\")\n2. If wrong client chunks returned → immediately use Contract_Direct_Lookup\n3. Contract controls over HubSpot fields — flag discrepancies\n\n---\n\n## INVOICE CREATION\n1. customerId — Find customer by name\n2. itemId — Find item by name\n3. docNumber — Get latest +1, collision-check vs CreditMemo and Estimate\n4. taxCodeId — MUS/MMX/MSAG only; ask if unspecified\nConfirm with user before creating.\n\n## INVOICE SEND\nAlways resolve email first via Find customer / Get customer. Never send with null email.\n\n## INVOICE UPDATE\nFetch invoice first (Id + SyncToken + full Line array). Build update from fetched lines only.\n\n---\n\n## RECLASS JOURNAL ENTRY\n1. Fetch original (Id + SyncToken)\n2. Look up target account IDs via chartofaccounts\n3. Confirm debit/credit table with user\n4. Void original via {ENTITY} - Void Journal Entry\n5. Post corrected entry — DocNumber = original + \"-R\", PrivateNote = \"Reclass of [DocNumber]\"\n\n---\n\n## JOURNAL ENTRY RULES\n- Debits must equal credits\n- Always look up account IDs via chartofaccounts first\n- Confirm full debit/credit table before posting\n- MHAG/MGC: JE only — no invoice or customer tools\n- Department/Class: look up Id via QBQL before using\n\nEntity map: MUS (USD) · MMX (USD) · MSAG (CHF) · MHAG (CHF, JE only) · MGC (USD, JE only)\n\n---\n\n## CURRENCY\n- Entity home currencies: MUS/MMX/MGC = USD · MSAG/MHAG = CHF\n- After any GL query with MSAG/MHAG rows: call Get FX Rate, convert to USD, note the rate\n\n---\n\n## LARGE DATASET HANDLING\n- All filtering/joining in SQL — never loop per customer\n- Display: LIMIT 50 OFFSET N batches; collect silently, format once\n- Hard cap: 25 rows in chat. >25 rows: offer export. >100 rows: Export_File mandatory\n\n## FILE EXPORT\n- Export_File: pass filename (snake_case, no extension) + complete SELECT sql\n- Never assemble rows in memory — always pass SQL directly\n- Return the message field from response verbatim\n\n---\n\n## MICROSOFT TEAMS\nYour userId: 94387110-efa9-4e7f-8f18-3332df67c0a9 · email: mnolan@masttro.com\nAlways exclude yourself when identifying the other person in a chat.\n\nDM: Get Teams DM Chats → match non-self member → Send Teams DM (once, no retry)\nGroup: Get Teams Group Chats → match topic/member → Send Teams DM (once)\nChannel: Get Teams List → Get Teams Channels → Send Teams Channel Message (once)\n\nNever guess a chatId. Plain text only — no markdown tables in Teams messages.\n\n---\n\n## FRESHDESK\nOpen tickets (no filter): use open finance/general support nodes; count from total field\nBy person: Contact Lookup → archive search with requester:email\nBy keyword: archive search with subject:keyword\nNo parentheses in query strings. Never use contact_name as search field.\n\n---\n\n## OUTPUT RULES\n- Plain markdown only — never HTML tables\n- Never surface tool calls, node names, or tool selection reasoning\n- Never use hardcoded year literals — always derive from NOW()\n- Always confirm destructive/write actions before executing\n- ILIKE for all case-insensitive Postgres searches\n- NEVER SELECT * without WHERE clause"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
15072,
4448
],
"id": "ff977ecc-c5e3-4d99-a890-c94405a2e433",
"name": "AI Agent"
},
{
"parameters": {},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.3,
"position": [
14992,
4624
],
"id": "531ee4d4-62e3-4bb1-8614-ef827ae57514",
"name": "Simple Memory"
},
{
"parameters": {
"toolDescription": "Fetch current exchange rates using exchangerate-api.com. Pass fromCurrency (e.g. USD) and the response will contain rates to all major currencies including CHF, EUR, etc.",
"url": "=https://api.exchangerate-api.com/v4/latest/{{ $fromAI('fromCurrency', 'Source currency code (e.g. USD, EUR, GBP)', '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": [
14784,
4880
],
"id": "5b0bf645-ee0c-4042-abad-af7517a3e10a",
"name": "Get FX Rate"
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the view_hubspot_closedwon Postgres view for HubSpot closed-won deals. ALWAYS write a SELECT with a WHERE clause and LIMIT.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT statement. MUST include a WHERE clause and a LIMIT (<= 500).', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
15952,
4400
],
"id": "f3157be8-57f6-40ce-b4ce-e4c50ee9668c",
"name": "Hubspot deals",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-haiku-4-5-20251001",
"mode": "list",
"cachedResultName": "Claude Haiku 4.5"
},
"options": {
"temperature": 0.2
}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
15584,
4608
],
"id": "1d2f5886-6d7b-44c1-8cdc-06da7ab0efab",
"name": "Haiku 4.5",
"credentials": {
"anthropicApi": {
"id": "uJ4J74wLCEWqVd9B",
"name": "Anthropic - Moneybot"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-6",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.6"
},
"options": {
"temperature": 0.2
}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
15376,
4592
],
"id": "02fce2ce-be02-4f2d-878b-85f2f71b87d5",
"name": "Sonnet 4.6",
"credentials": {
"anthropicApi": {
"id": "uJ4J74wLCEWqVd9B",
"name": "Anthropic - Moneybot"
}
}
},
{
"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": 20,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.vectorStorePGVector",
"typeVersion": 1.3,
"position": [
15648,
4464
],
"id": "718be9dc-c636-4f80-9261-6cc7c8f02588",
"name": "Postgres PGVector Store",
"credentials": {
"postgres": {
"id": "YrHntSw7KF7Za0tK",
"name": "Postgres vectors"
}
}
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.embeddingsMistralCloud",
"typeVersion": 1,
"position": [
15776,
4592
],
"id": "cf033d73-5533-4ca2-96ef-497dea1e4b18",
"name": "mistral-embed",
"credentials": {
"mistralCloudApi": {
"id": "lHc1GsczHEq7NL55",
"name": "Mistral - contracts vector"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Direct SQL lookup of contract chunks from the contracts table by client name and/or section heading. Use as FALLBACK when pgvector returns chunks from the wrong client.\n\nExample: SELECT text, section_heading, page_number FROM contracts WHERE client_name ILIKE '%49 Management%' ORDER BY page_number ASC, id ASC LIMIT 50",
"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": [
15952,
4560
],
"id": "684b8901-931f-491b-90cd-404e647a5936",
"name": "Contract_Direct_Lookup",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "\"status:2 OR status:3\""
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15312,
4864
],
"id": "c6ec24f1-1b21-4fce-ae7b-5901681c377e",
"name": "Freshdesk - open finance tickets",
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"url": "https://masttro-assist.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "\"status:2 OR status:3\""
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15120,
4864
],
"id": "61c96080-1240-46f4-a474-8feb2c0c7c1d",
"name": "Freshdesk - open general support tickets",
"credentials": {
"freshdeskApi": {
"id": "eU5oyV62tEJdY6dV",
"name": "Freshdesk - general support"
}
}
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('search_query', 'Freshdesk search query string. Examples: \"requester:email@domain.com\", \"subject:invoice\". Always wrap in double quotes.', 'string') }}"
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15520,
4848
],
"id": "d0015e34-fe26-4915-b11f-f9eccd92a000",
"name": "Freshdesk archive - finance",
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"url": "https://masttro-assist.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('search_query', 'Freshdesk search query string. Examples: \"requester:email@domain.com\", \"subject:invoice\". Always wrap in double quotes.', 'string') }}"
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15712,
4848
],
"id": "d81d88b1-d9ec-4494-8c17-04562a7022fa",
"name": "Freshdesk archive - general support",
"credentials": {
"freshdeskApi": {
"id": "eU5oyV62tEJdY6dV",
"name": "Freshdesk - general support"
}
}
},
{
"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.', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15904,
4848
],
"id": "464ce97a-b552-4283-8524-73094ce05f5c",
"name": "Freshdesk Contact Lookup - Finance",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"url": "https://masttro-assist.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.', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
16112,
4832
],
"id": "35869ee7-d81e-4d14-b476-e273c2a591fc",
"name": "Freshdesk Contact Lookup - General Support",
"credentials": {
"httpBasicAuth": {
"id": "nTAuFBMj691S2ltO",
"name": "Freshdesk - general support"
}
}
},
{
"parameters": {
"toolDescription": "Exports query results to a CSV file and returns a download URL. Pass a complete SELECT SQL query and a filename.",
"method": "POST",
"url": "https://moneybot.masttro.com/webhook/export-insert",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "filename",
"value": "={{ $fromAI('filename', 'CSV filename without extension, snake_case, e.g. contracts_renewal_2026-05-14') }}"
},
{
"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": [
14944,
4880
],
"id": "e3402386-a250-40c0-85af-43565fd8e04d",
"name": "Export_File"
},
{
"parameters": {
"httpMethod": "POST",
"path": "contractbot",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
14384,
4160
],
"id": "ea3b9e9a-0b4f-4c4c-ae3c-9f0bac57f118",
"name": "Webhook",
"webhookId": "8d835ff1-ebd9-471f-87e8-fbcb3eb3c6ef"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "map-message",
"name": "chatInput",
"value": "={{ $json.body.message }}",
"type": "string"
},
{
"id": "map-sender",
"name": "sender",
"value": "={{ $json.body.sender }}",
"type": "string"
},
{
"id": "map-senderid",
"name": "senderId",
"value": "={{ $json.body.senderId }}",
"type": "string"
},
{
"id": "map-messageid",
"name": "messageId",
"value": "={{ $json.body.messageId }}",
"type": "string"
},
{
"id": "map-channelid",
"name": "channelId",
"value": "={{ $json.body.channelId }}",
"type": "string"
},
{
"id": "map-teamid",
"name": "teamId",
"value": "={{ $json.body.teamId }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
14640,
4144
],
"id": "a00987de-5146-4b26-883b-8ebc97f48ca9",
"name": "Map Teams Payload"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ reply: $json.output }) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
15472,
4432
],
"id": "5b755b0a-597f-460e-b475-a69e60e05512",
"name": "Respond to Teams"
},
{
"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": [
16224,
4416
],
"id": "eed2ede6-f2ee-40df-98a3-31eaed798c7c",
"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"
}
}
},
{
"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": [
16496,
4576
],
"id": "daf510e2-a1a0-4660-9e4e-f2d39adfaf63",
"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"
}
}
},
{
"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": [
16224,
4576
],
"id": "97b32ae3-68d4-44a0-ab23-7ca45daa250b",
"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"
}
}
},
{
"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": [
16400,
4416
],
"id": "eb3a53df-8ace-4702-af98-20abca1c226a",
"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"
}
}
},
{
"parameters": {
"resource": "chatMessage",
"chatId": {
"__rl": true,
"value": "={{ $fromAI('chatId', 'The Teams chat GUID resolved from Get Teams DM Chats or Get Teams Group Chats. 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": [
16560,
4368
],
"id": "068406e6-73c8-4aef-848b-540aedd534ba",
"name": "Send Teams DM",
"webhookId": "d0bf3351-3696-4ea0-adbd-ddec03926981",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account"
}
}
},
{
"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.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.microsoftTeamsTool",
"typeVersion": 2,
"position": [
16688,
4560
],
"id": "3fd0e16b-f259-47e5-abe5-76635d40b7b2",
"name": "Send Teams Channel Message",
"webhookId": "fb75f216-5c71-4ea0-867b-90abb819a51c",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account"
}
}
},
{
"parameters": {
"content": "# Microsoft Teams",
"height": 400,
"width": 768
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
16144,
4320
],
"id": "4c2841ea-0be5-4485-9886-f974393c2d8b",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## Teams Webhook Path\n\nPower Automate POSTs to:\nhttps://moneybot.masttro.com/webhook/contractbot\n\nPayload: { message, sender, senderId, messageId, channelId, teamId }\n\nReply is returned as JSON: { reply: '...' }\nPower Automate reads body.reply and posts back to Teams.",
"height": 344,
"width": 500
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
14320,
3952
],
"id": "e7e6b1b9-61dc-4a19-b348-4e86dc645579",
"name": "Teams Webhook Note"
}
],
"connections": {
"When chat message received": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Map Teams Payload",
"type": "main",
"index": 0
}
]
]
},
"Map Teams Payload": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Respond to Teams",
"type": "main",
"index": 0
}
]
]
},
"Simple Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"Get FX Rate": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot deals": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Sonnet 4.6": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"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
}
]
]
},
"Freshdesk - open finance tickets": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - open general support tickets": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk archive - finance": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk archive - general support": {
"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 Contact Lookup - General Support": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Export_File": {
"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
}
]
]
}
},
"settings": {
"binaryMode": "separate",
"errorWorkflow": "tJSgL4iy63ys2NCA",
"availableInMCP": false,
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"pinData": {},
"versionId": "679315e7-61ef-4b87-9b1e-5d6fe1b80544",
"activeVersionId": "679315e7-61ef-4b87-9b1e-5d6fe1b80544",
"triggerCount": 2,
"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-05-22T15:22:04.438Z",
"createdAt": "2026-05-22T15:22:02.573Z",
"versionId": "679315e7-61ef-4b87-9b1e-5d6fe1b80544",
"workflowId": "eoMLOlRefzddaMQs",
"nodes": [
{
"parameters": {
"public": true,
"authentication": "basicAuth",
"initialMessages": "💰 MoneyBot online.\nI'm limited to querying 1,200+ contracts and amendments. I can export to csv too.",
"availableInChat": true,
"agentName": "Moneybot",
"options": {
"allowFileUploads": true
}
},
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1.4,
"position": [
14816,
4576
],
"id": "d7a24034-2a15-4d6d-a013-434b70d7cb7a",
"name": "When chat message received",
"webhookId": "672bf651-91e6-41de-b897-13e54c0e1f10",
"alwaysOutputData": true,
"credentials": {
"httpBasicAuth": {
"id": "zqSwWx9eXcAOQ4yE",
"name": "Masttr0! | Masttr0! basic auth"
}
}
},
{
"parameters": {
"content": "## Internal link using basic auth\n\nhttps://Masttr0!:Masttr0!@moneybot.masttro.com/webhook/672bf651-91e6-41de-b897-13e54c0e1f10/chat",
"height": 176,
"width": 464
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
15248,
4048
],
"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": 400,
"width": 1324,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
14768,
4336
],
"id": "9e4efe01-5b4b-44a4-9921-91aee0647e51",
"name": "Sticky Core"
},
{
"parameters": {
"options": {
"systemMessage": "=Current datetime is {{ $now }}\n\nYou are MoneyBot, a finance automation assistant for Masttro. You have access to:\n- QuickBooks Online: MUS (realm 9341452327761715, USD), MMX (realm 9341452328017372, USD), MSAG (realm 9341452373926753, CHF), MHAG (realm 9341452561201469, CHF — JE only), MGC (realm 9130354300064436, USD — JE only)\n- Postgres data warehouse: open invoices, payments, general ledger, HubSpot deals, chart of accounts, contracts\n- Freshdesk (support tickets)\n- FX Rate lookup, EML file generation, Teams messaging\n\n---\n\n## TOOL SELECTION\n\n| Intent | Tool |\n|---|---|\n| Open/unpaid invoices | Postgres: view_invoices_open |\n| Payments received | Postgres: view_payments |\n| General ledger / revenue | Postgres: AI_view_general_ledger |\n| Account ID lookup | Postgres: chartofaccounts — ALWAYS before JE |\n| HubSpot pipeline | Postgres: hubspot_deals |\n| HubSpot closed-won + billing | Postgres: view_hubspot_closedwon |\n| HubSpot owner lookup | Postgres: hubspot_owners |\n| Billing schedule | Postgres: view_hubspot_closedwon — see BILLING SCHEDULE |\n| Next invoice number | {ENTITY} - Get latest invoice DocNumber +1, collision-check |\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) — always add WHERE + MAXRESULTS |\n| Create journal entry | {ENTITY} - Create Journal Entry — verify debits = credits |\n| Void journal entry | Fetch Id + SyncToken first, confirm, then {ENTITY} - Void Journal Entry |\n| Export data | Export_File — pass filename + sql |\n| Contract list / terms | Postgres: Contract_Direct_Lookup WHERE page_number = 1 |\n| Contract clause lookup | pgvector store first; fallback to Contract_Direct_Lookup |\n| Teams DM | Get Teams DM Chats → Send Teams DM |\n| Teams channel post | Get Teams List → Get Teams Channels → Send Teams Channel Message |\n\nNever dump all invoices/customers/payments from QBO. Always use Postgres views for list queries.\n\n---\n\n## CHART OF ACCOUNTS\nAlways query chartofaccounts before any JE — never use QBQL for account ID lookups.\nFilter: company_id, \"Active\" = true, match \"Name\" ILIKE or \"AcctNum\" ILIKE.\naccount_id is text — pass as string in AccountRef. Never cast.\n\n---\n\n## QBQL SYNTAX — critical\n- NO IN(...), NO OR, NO LIKE/ILIKE, NO AcctNum filter, NO subqueries, NO JOINs\n- Multiple values: separate tool calls per value\n- ORDERBY (no space), MAXRESULTS (not LIMIT), cap at 100\n- String values: single quotes only\n\n---\n\n## POSTGRES COLUMN RULES\n\n**view_invoices_open** — all lowercase. txndate is text; cast for arithmetic: txndate::date\n\n**view_payments** — company_id lowercase; all others double-quoted.\n\"TxnDate\" is DATE — use: \"TxnDate\" >= CURRENT_DATE - INTERVAL '7 days'\nNever compare to text literal.\n\n**AI_view_general_ledger** — exactly 6 columns: company_id, \"Date\", \"Memo_Description\", amount, \"Account\", \"Classification\"\n\"Date\" is text/varchar — always cast: \"Date\"::date for any date function.\nDate filter: \"Date\" >= TO_CHAR(DATE_TRUNC('year', NOW()), 'YYYY-MM-DD')\nNever use DATE_TRUNC(...)::text — produces timestamp string, breaks text compare.\nGROUP BY: repeat full expression, never alias.\nRevenue posts negative — always ABS(SUM(amount)).\nClassifications: Revenue, Expense, Asset, Liability, Equity\n\n**contracts** — all lowercase. Reference as just: contracts (no schema prefix).\nUse page_number = 1 for one row per contract.\n\n**hubspot_deals** — deal_name (underscore). No deal_url or deal_currency_code here — use view_hubspot_closedwon.\namount_in_home_currency = USD, use for cross-currency aggregations.\n\n**view_hubspot_closedwon** — filters to is_closed_won = true already. Always include deal_url in results.\n\n---\n\n## BILLING SCHEDULE\nSource: view_hubspot_closedwon. Billing frequency from contract term:\n- Monthly → first of each month in range\n- Quarterly/Semi-annual → anniversary-based from contract_start_date\n- Annual → contract_start_date anniversary in current year\n- NULL term → retrieve contract from pgvector/Contract_Direct_Lookup before defaulting\n\n---\n\n## CONTRACT RETRIEVAL\n1. pgvector store first — use specific clause language (\"semi-annual payment schedule\", \"termination notice period\")\n2. If wrong client chunks returned → immediately use Contract_Direct_Lookup\n3. Contract controls over HubSpot fields — flag discrepancies\n\n---\n\n## INVOICE CREATION\n1. customerId — Find customer by name\n2. itemId — Find item by name\n3. docNumber — Get latest +1, collision-check vs CreditMemo and Estimate\n4. taxCodeId — MUS/MMX/MSAG only; ask if unspecified\nConfirm with user before creating.\n\n## INVOICE SEND\nAlways resolve email first via Find customer / Get customer. Never send with null email.\n\n## INVOICE UPDATE\nFetch invoice first (Id + SyncToken + full Line array). Build update from fetched lines only.\n\n---\n\n## RECLASS JOURNAL ENTRY\n1. Fetch original (Id + SyncToken)\n2. Look up target account IDs via chartofaccounts\n3. Confirm debit/credit table with user\n4. Void original via {ENTITY} - Void Journal Entry\n5. Post corrected entry — DocNumber = original + \"-R\", PrivateNote = \"Reclass of [DocNumber]\"\n\n---\n\n## JOURNAL ENTRY RULES\n- Debits must equal credits\n- Always look up account IDs via chartofaccounts first\n- Confirm full debit/credit table before posting\n- MHAG/MGC: JE only — no invoice or customer tools\n- Department/Class: look up Id via QBQL before using\n\nEntity map: MUS (USD) · MMX (USD) · MSAG (CHF) · MHAG (CHF, JE only) · MGC (USD, JE only)\n\n---\n\n## CURRENCY\n- Entity home currencies: MUS/MMX/MGC = USD · MSAG/MHAG = CHF\n- After any GL query with MSAG/MHAG rows: call Get FX Rate, convert to USD, note the rate\n\n---\n\n## LARGE DATASET HANDLING\n- All filtering/joining in SQL — never loop per customer\n- Display: LIMIT 50 OFFSET N batches; collect silently, format once\n- Hard cap: 25 rows in chat. >25 rows: offer export. >100 rows: Export_File mandatory\n\n## FILE EXPORT\n- Export_File: pass filename (snake_case, no extension) + complete SELECT sql\n- Never assemble rows in memory — always pass SQL directly\n- Return the message field from response verbatim\n\n---\n\n## MICROSOFT TEAMS\nYour userId: 94387110-efa9-4e7f-8f18-3332df67c0a9 · email: mnolan@masttro.com\nAlways exclude yourself when identifying the other person in a chat.\n\nDM: Get Teams DM Chats → match non-self member → Send Teams DM (once, no retry)\nGroup: Get Teams Group Chats → match topic/member → Send Teams DM (once)\nChannel: Get Teams List → Get Teams Channels → Send Teams Channel Message (once)\n\nNever guess a chatId. Plain text only — no markdown tables in Teams messages.\n\n---\n\n## FRESHDESK\nOpen tickets (no filter): use open finance/general support nodes; count from total field\nBy person: Contact Lookup → archive search with requester:email\nBy keyword: archive search with subject:keyword\nNo parentheses in query strings. Never use contact_name as search field.\n\n---\n\n## OUTPUT RULES\n- Plain markdown only — never HTML tables\n- Never surface tool calls, node names, or tool selection reasoning\n- Never use hardcoded year literals — always derive from NOW()\n- Always confirm destructive/write actions before executing\n- ILIKE for all case-insensitive Postgres searches\n- NEVER SELECT * without WHERE clause"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
15072,
4448
],
"id": "ff977ecc-c5e3-4d99-a890-c94405a2e433",
"name": "AI Agent"
},
{
"parameters": {},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.3,
"position": [
14992,
4624
],
"id": "531ee4d4-62e3-4bb1-8614-ef827ae57514",
"name": "Simple Memory"
},
{
"parameters": {
"toolDescription": "Fetch current exchange rates using exchangerate-api.com. Pass fromCurrency (e.g. USD) and the response will contain rates to all major currencies including CHF, EUR, etc.",
"url": "=https://api.exchangerate-api.com/v4/latest/{{ $fromAI('fromCurrency', 'Source currency code (e.g. USD, EUR, GBP)', '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": [
14784,
4880
],
"id": "5b0bf645-ee0c-4042-abad-af7517a3e10a",
"name": "Get FX Rate"
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Query the view_hubspot_closedwon Postgres view for HubSpot closed-won deals. ALWAYS write a SELECT with a WHERE clause and LIMIT.",
"operation": "executeQuery",
"query": "{{ $fromAI('sql', 'A complete SELECT statement. MUST include a WHERE clause and a LIMIT (<= 500).', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.postgresTool",
"typeVersion": 2.6,
"position": [
15952,
4400
],
"id": "f3157be8-57f6-40ce-b4ce-e4c50ee9668c",
"name": "Hubspot deals",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-haiku-4-5-20251001",
"mode": "list",
"cachedResultName": "Claude Haiku 4.5"
},
"options": {
"temperature": 0.2
}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
15584,
4608
],
"id": "1d2f5886-6d7b-44c1-8cdc-06da7ab0efab",
"name": "Haiku 4.5",
"credentials": {
"anthropicApi": {
"id": "uJ4J74wLCEWqVd9B",
"name": "Anthropic - Moneybot"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-6",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.6"
},
"options": {
"temperature": 0.2
}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
15376,
4592
],
"id": "02fce2ce-be02-4f2d-878b-85f2f71b87d5",
"name": "Sonnet 4.6",
"credentials": {
"anthropicApi": {
"id": "uJ4J74wLCEWqVd9B",
"name": "Anthropic - Moneybot"
}
}
},
{
"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": 20,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.vectorStorePGVector",
"typeVersion": 1.3,
"position": [
15648,
4464
],
"id": "718be9dc-c636-4f80-9261-6cc7c8f02588",
"name": "Postgres PGVector Store",
"credentials": {
"postgres": {
"id": "YrHntSw7KF7Za0tK",
"name": "Postgres vectors"
}
}
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.embeddingsMistralCloud",
"typeVersion": 1,
"position": [
15776,
4592
],
"id": "cf033d73-5533-4ca2-96ef-497dea1e4b18",
"name": "mistral-embed",
"credentials": {
"mistralCloudApi": {
"id": "lHc1GsczHEq7NL55",
"name": "Mistral - contracts vector"
}
}
},
{
"parameters": {
"descriptionType": "manual",
"toolDescription": "Direct SQL lookup of contract chunks from the contracts table by client name and/or section heading. Use as FALLBACK when pgvector returns chunks from the wrong client.\n\nExample: SELECT text, section_heading, page_number FROM contracts WHERE client_name ILIKE '%49 Management%' ORDER BY page_number ASC, id ASC LIMIT 50",
"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": [
15952,
4560
],
"id": "684b8901-931f-491b-90cd-404e647a5936",
"name": "Contract_Direct_Lookup",
"credentials": {
"postgres": {
"id": "NoPrLWbqySMMmIaI",
"name": "Postgres n8n-quickbooks"
}
}
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "\"status:2 OR status:3\""
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15312,
4864
],
"id": "c6ec24f1-1b21-4fce-ae7b-5901681c377e",
"name": "Freshdesk - open finance tickets",
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"url": "https://masttro-assist.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "\"status:2 OR status:3\""
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15120,
4864
],
"id": "61c96080-1240-46f4-a474-8feb2c0c7c1d",
"name": "Freshdesk - open general support tickets",
"credentials": {
"freshdeskApi": {
"id": "eU5oyV62tEJdY6dV",
"name": "Freshdesk - general support"
}
}
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('search_query', 'Freshdesk search query string. Examples: \"requester:email@domain.com\", \"subject:invoice\". Always wrap in double quotes.', 'string') }}"
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15520,
4848
],
"id": "d0015e34-fe26-4915-b11f-f9eccd92a000",
"name": "Freshdesk archive - finance",
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"url": "https://masttro-assist.freshdesk.com/api/v2/search/tickets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "freshdeskApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $fromAI('search_query', 'Freshdesk search query string. Examples: \"requester:email@domain.com\", \"subject:invoice\". Always wrap in double quotes.', 'string') }}"
},
{
"name": "page",
"value": "1"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15712,
4848
],
"id": "d81d88b1-d9ec-4494-8c17-04562a7022fa",
"name": "Freshdesk archive - general support",
"credentials": {
"freshdeskApi": {
"id": "eU5oyV62tEJdY6dV",
"name": "Freshdesk - general support"
}
}
},
{
"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.', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
15904,
4848
],
"id": "464ce97a-b552-4283-8524-73094ce05f5c",
"name": "Freshdesk Contact Lookup - Finance",
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"url": "https://masttro-assist.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.', 'string') }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
16112,
4832
],
"id": "35869ee7-d81e-4d14-b476-e273c2a591fc",
"name": "Freshdesk Contact Lookup - General Support",
"credentials": {
"httpBasicAuth": {
"id": "nTAuFBMj691S2ltO",
"name": "Freshdesk - general support"
}
}
},
{
"parameters": {
"toolDescription": "Exports query results to a CSV file and returns a download URL. Pass a complete SELECT SQL query and a filename.",
"method": "POST",
"url": "https://moneybot.masttro.com/webhook/export-insert",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "filename",
"value": "={{ $fromAI('filename', 'CSV filename without extension, snake_case, e.g. contracts_renewal_2026-05-14') }}"
},
{
"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": [
14944,
4880
],
"id": "e3402386-a250-40c0-85af-43565fd8e04d",
"name": "Export_File"
},
{
"parameters": {
"httpMethod": "POST",
"path": "contractbot",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
14384,
4160
],
"id": "ea3b9e9a-0b4f-4c4c-ae3c-9f0bac57f118",
"name": "Webhook",
"webhookId": "8d835ff1-ebd9-471f-87e8-fbcb3eb3c6ef"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "map-message",
"name": "chatInput",
"value": "={{ $json.body.message }}",
"type": "string"
},
{
"id": "map-sender",
"name": "sender",
"value": "={{ $json.body.sender }}",
"type": "string"
},
{
"id": "map-senderid",
"name": "senderId",
"value": "={{ $json.body.senderId }}",
"type": "string"
},
{
"id": "map-messageid",
"name": "messageId",
"value": "={{ $json.body.messageId }}",
"type": "string"
},
{
"id": "map-channelid",
"name": "channelId",
"value": "={{ $json.body.channelId }}",
"type": "string"
},
{
"id": "map-teamid",
"name": "teamId",
"value": "={{ $json.body.teamId }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
14640,
4144
],
"id": "a00987de-5146-4b26-883b-8ebc97f48ca9",
"name": "Map Teams Payload"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ reply: $json.output }) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
15472,
4432
],
"id": "5b755b0a-597f-460e-b475-a69e60e05512",
"name": "Respond to Teams"
},
{
"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": [
16224,
4416
],
"id": "eed2ede6-f2ee-40df-98a3-31eaed798c7c",
"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"
}
}
},
{
"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": [
16496,
4576
],
"id": "daf510e2-a1a0-4660-9e4e-f2d39adfaf63",
"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"
}
}
},
{
"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": [
16224,
4576
],
"id": "97b32ae3-68d4-44a0-ab23-7ca45daa250b",
"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"
}
}
},
{
"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": [
16400,
4416
],
"id": "eb3a53df-8ace-4702-af98-20abca1c226a",
"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"
}
}
},
{
"parameters": {
"resource": "chatMessage",
"chatId": {
"__rl": true,
"value": "={{ $fromAI('chatId', 'The Teams chat GUID resolved from Get Teams DM Chats or Get Teams Group Chats. 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": [
16560,
4368
],
"id": "068406e6-73c8-4aef-848b-540aedd534ba",
"name": "Send Teams DM",
"webhookId": "d0bf3351-3696-4ea0-adbd-ddec03926981",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account"
}
}
},
{
"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.', 'string') }}",
"options": {}
},
"type": "n8n-nodes-base.microsoftTeamsTool",
"typeVersion": 2,
"position": [
16688,
4560
],
"id": "3fd0e16b-f259-47e5-abe5-76635d40b7b2",
"name": "Send Teams Channel Message",
"webhookId": "fb75f216-5c71-4ea0-867b-90abb819a51c",
"credentials": {
"microsoftTeamsOAuth2Api": {
"id": "VGkMYU2FoGJszbIJ",
"name": "Microsoft Teams account"
}
}
},
{
"parameters": {
"content": "# Microsoft Teams",
"height": 400,
"width": 768
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
16144,
4320
],
"id": "4c2841ea-0be5-4485-9886-f974393c2d8b",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## Teams Webhook Path\n\nPower Automate POSTs to:\nhttps://moneybot.masttro.com/webhook/contractbot\n\nPayload: { message, sender, senderId, messageId, channelId, teamId }\n\nReply is returned as JSON: { reply: '...' }\nPower Automate reads body.reply and posts back to Teams.",
"height": 344,
"width": 500
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
14320,
3952
],
"id": "e7e6b1b9-61dc-4a19-b348-4e86dc645579",
"name": "Teams Webhook Note"
}
],
"connections": {
"When chat message received": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Map Teams Payload",
"type": "main",
"index": 0
}
]
]
},
"Map Teams Payload": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Respond to Teams",
"type": "main",
"index": 0
}
]
]
},
"Simple Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"Get FX Rate": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Hubspot deals": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Sonnet 4.6": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"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
}
]
]
},
"Freshdesk - open finance tickets": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk - open general support tickets": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk archive - finance": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Freshdesk archive - general support": {
"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 Contact Lookup - General Support": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Export_File": {
"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
}
]
]
}
},
"nodeGroups": [],
"authors": "Michael Nolan",
"name": "Version 679315e7",
"description": "",
"autosaved": false
},
"tags": []
}