{
  "id": "FChFYOPW7ReutJkS",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Cash Reconciliation Checker",
  "tags": [],
  "nodes": [
    {
      "id": "667627cb-1d7c-4681-8364-c1efb7f768e5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        944,
        544
      ],
      "parameters": {
        "width": 704,
        "height": 624,
        "content": "## Cash Reconciliation Workflow\n\nAutomates balance reconciliation by comparing internal and custodian cash balances, identifying mismatches and routing records for logging, AI-based analysis and Slack alerts.\n\n## Flow:\n\nSchedule Trigger \u2192 Fetch Sheets \u2192 Edit Fields \u2192 Match Accounts \u2192 Calculate Difference \u2192 Mismatch Check\n\n## Setup Steps\n\nCreate two Google Sheets for internal and custodian balances using the same account_id values. Ensure balance columns contain numeric data only. Configure Schedule Trigger timing, map fields correctly in Google Sheets nodes and connect Slack for mismatch alerts. Verify AI node, logging sheet and output columns are properly configured.\n\n## How It Works\n\nThe workflow runs on schedule, fetches both balance sheets, standardizes fields and matches records by account_id. It calculates the balance difference and checks for mismatches. Matching records are logged directly. Mismatched records go through AI explanation, are prepared for reporting, logged into Sheets and sent as Slack alerts."
      },
      "typeVersion": 1
    },
    {
      "id": "589ada80-e623-41a0-a14d-480e9758e1db",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        1456
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 528,
        "content": "## Scheduled Balance Fetch\n\nTriggers the workflow on a schedule and pulls both internal finance balances and custodian/bank balances from Google Sheets for reconciliation."
      },
      "typeVersion": 1
    },
    {
      "id": "f0af182d-1077-4a37-b5f5-b2fece0bcd61",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2992,
        1456
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 528,
        "content": "## Reconciliation Outcome Check\n\nEvaluates the calculated balance difference and routes each record based on whether it is **matched** or **mismatched** for further logging and exception handling.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "1d9ea856-87b8-491a-a394-a77ba032f117",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3392,
        1456
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 528,
        "content": "## AI Exception Analysis\n\nTakes mismatched reconciliation records, sends them to the AI model for a short possible explanation and prepares the result for exception logging and alerting.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "91596983-4ce7-4ba0-828f-70e308a316c6",
      "name": "Run Reconciliation on Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1680,
        1728
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "86dba853-c610-498a-9c6e-eb60a83d24f4",
      "name": "Fetch Internal Balances",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1888,
        1632
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1aybRV31sefahegSLnKXYlg0f9NyId4e-6_M3KUzRRTk",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1aybRV31sefahegSLnKXYlg0f9NyId4e-6_M3KUzRRTk/edit?usp=drivesdk",
          "cachedResultName": "Internal Balance Sheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "13d797e3-7816-4bf3-b9c1-8621722be228",
      "name": "Fetch Custodian Balances",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1888,
        1824
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1m2VP3OdOJr4HgzWO_VjgeC9H-EXTjm8AtrbgYEu5xyI/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1m2VP3OdOJr4HgzWO_VjgeC9H-EXTjm8AtrbgYEu5xyI",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1m2VP3OdOJr4HgzWO_VjgeC9H-EXTjm8AtrbgYEu5xyI/edit?usp=drivesdk",
          "cachedResultName": "Custodian Balance Sheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "4f6108e9-8356-4658-9ca9-2b64421a5846",
      "name": "Match Accounts by Account ID",
      "type": "n8n-nodes-base.merge",
      "position": [
        2336,
        1728
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "account_id"
      },
      "typeVersion": 3.2
    },
    {
      "id": "3f2f63f4-8010-46a6-9d25-7efd94560174",
      "name": "Calculate Balance Difference",
      "type": "n8n-nodes-base.code",
      "position": [
        2560,
        1728
      ],
      "parameters": {
        "jsCode": "const TOLERANCE = 0.01;\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const d = item.json;\n  const internal = parseFloat(d.internal_balance ?? 'NaN');\n  const custodian = parseFloat(d.custodian_balance ?? 'NaN');\n  const difference = parseFloat((internal - custodian).toFixed(6));\n  const absDiff = Math.abs(difference);\n  const mismatch = isNaN(internal) || isNaN(custodian) || absDiff > TOLERANCE;\n\n  results.push({\n    json: {\n      account_id: d.account_id,\n      currency: d.currency,\n      internal_balance: internal,\n      custodian_balance: custodian,\n      difference: difference,\n      abs_difference: absDiff,\n      mismatch: mismatch,\n      checked_at: new Date().toISOString()\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "92daf1ee-4b9e-4bb5-a339-6b4f8c7a5f54",
      "name": "Check for Balance Mismatch",
      "type": "n8n-nodes-base.if",
      "position": [
        2784,
        1728
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "4422433f-2a60-445d-a376-b8f3a1c8e42d",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.mismatch }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "f9c44d76-b6a6-40fe-9587-f48bf60c4e64",
      "name": "Generate AI Mismatch Explanation",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3056,
        1648
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "content": "=You are a senior operations analyst at a fund administrator.\n\nAccount: {{ $json.account_id }}\nCurrency: {{ $json.currency }}\nInternal balance: {{ $json.internal_balance }}\nCustodian balance: {{ $json.custodian_balance }}\nDifference (internal minus custodian): {{ $json.difference }}\nCheck date: {{ $json.checked_at }}\n\nIn maximum 120 words, explain the most likely cause of this mismatch from: settlement delay (T+1/T+2), pending fees or accrued interest, FX conversion timing, failed corporate actions, bank charges not yet booked or data entry error. State top 1-2 causes and end with one recommended next action."
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "5ca9a11d-342f-43d8-9601-95ea58c83f46",
      "name": "Prepare Exception Record",
      "type": "n8n-nodes-base.code",
      "position": [
        3488,
        1664
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item, index) => {\n  const aiText = item.json.output?.[0]?.content?.[0]?.text ?? 'No explanation returned.';\n  \n  // Match by index, not hardcoded [0]\n  const original = $('Check for Balance Mismatch').all()[index].json;\n\n  return {\n    json: {\n      account_id: original.account_id,\n      currency: original.currency,\n      internal_balance: original.internal_balance,\n      custodian_balance: original.custodian_balance,\n      difference: original.difference,\n      abs_difference: original.abs_difference,\n      mismatch: original.mismatch,\n      checked_at: original.checked_at,\n      ai_explanation: aiText,\n      recon_status: 'Mismatch'\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "8986cc9c-ad55-4d4e-85c2-ed508d7250ac",
      "name": "Log Matched Records",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3104,
        1824
      ],
      "parameters": {
        "columns": {
          "value": {
            "currency": "={{ $json.currency }}",
            "mismatch": "={{ $json.mismatch }}",
            "account_id": "={{ $json.account_id }}",
            "checked_at": "={{ $json.checked_at }}",
            "difference": "={{ $json.difference }}",
            "recon_status": "Matched",
            "abs_difference": "={{ $json.abs_difference }}",
            "internal_balance": "={{ $json.internal_balance }}",
            "custodian_balance": "={{ $json.custodian_balance }}"
          },
          "schema": [
            {
              "id": "account_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "account_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "currency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "currency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "internal_balance",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "internal_balance",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "custodian_balance",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "custodian_balance",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "difference",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "difference",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "abs_difference",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "abs_difference",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "mismatch",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "mismatch",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "checked_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "checked_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ai_explanation",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "ai_explanation",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "recon_status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "recon_status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1cG_BW9jZLFphiMLuIvRj4xh3bML2Eh_KbB9Dswln3NQ",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1cG_BW9jZLFphiMLuIvRj4xh3bML2Eh_KbB9Dswln3NQ/edit?usp=drivesdk",
          "cachedResultName": "Reconciliation Log Sheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "531e3349-2a4e-4824-b12d-38084bd54b29",
      "name": "Edit Internal Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        2112,
        1632
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "863617b6-3339-4dc1-8617-ca0a796c8fae",
              "name": "account_id",
              "type": "string",
              "value": "={{ $json.account_id }}"
            },
            {
              "id": "0d3a6dc2-30e1-4952-ad28-b025ebaba2d4",
              "name": "currency",
              "type": "string",
              "value": "={{ $json.currency }}"
            },
            {
              "id": "c89f8442-c1f5-4a3d-8084-d19a941c3e86",
              "name": "internal_balance",
              "type": "number",
              "value": "={{ Number($json.internal_balance) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "79239cec-2d78-4de7-bc06-639d82947fa4",
      "name": "Edit Custodian Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        2112,
        1824
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "bcd27321-c626-4bec-a439-92628fe0d5c3",
              "name": "account_id",
              "type": "string",
              "value": "={{ $json.account_id }}"
            },
            {
              "id": "9488e96d-9628-4ea8-9f18-8cda962b479f",
              "name": "currency",
              "type": "string",
              "value": "={{ $json.currency }}"
            },
            {
              "id": "842e8f40-d406-401c-a5d6-aca09ce75206",
              "name": "custodian_balance",
              "type": "number",
              "value": "={{ Number($json.custodian_balance) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "5f7b523a-720f-471e-a2e5-dbe6590ad55a",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2064,
        1456
      ],
      "parameters": {
        "color": 7,
        "width": 832,
        "height": 528,
        "content": "## Balance Comparison Logic\n\nFormats both balance sheets, matches accounts by `account_id`, calculates balance differences and detects mismatches for reconciliation."
      },
      "typeVersion": 1
    },
    {
      "id": "fc412596-8212-470a-b387-461e1fe9da91",
      "name": "Append The Data In The Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3680,
        1664
      ],
      "parameters": {
        "columns": {
          "value": {
            "currency": "={{ $json.currency }}",
            "mismatch": "={{ $json.mismatch }}",
            "account_id": "={{ $json.account_id }}",
            "checked_at": "={{ $json.checked_at }}",
            "difference": "={{ $json.difference }}",
            "recon_status": "={{ $json.recon_status }}",
            "abs_difference": "={{ $json.abs_difference }}",
            "ai_explanation": "={{ $json.ai_explanation }}",
            "internal_balance": "={{ $json.internal_balance }}",
            "custodian_balance": "={{ $json.custodian_balance }}"
          },
          "schema": [
            {
              "id": "account_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "account_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "currency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "currency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "internal_balance",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "internal_balance",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "custodian_balance",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "custodian_balance",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "difference",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "difference",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "abs_difference",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "abs_difference",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "mismatch",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "mismatch",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "checked_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "checked_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ai_explanation",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ai_explanation",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "recon_status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "recon_status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Vyewa8rH1WQ1d1FC9DAw7yuI6O8RRl0EcvZma6Xr4Ck/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Vyewa8rH1WQ1d1FC9DAw7yuI6O8RRl0EcvZma6Xr4Ck",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Vyewa8rH1WQ1d1FC9DAw7yuI6O8RRl0EcvZma6Xr4Ck/edit?usp=drivesdk",
          "cachedResultName": "alert_sheet"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "05871395-09c7-409b-912d-d84e297542e3",
  "connections": {
    "Edit Internal Fields": {
      "main": [
        [
          {
            "node": "Match Accounts by Account ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Custodian Fields": {
      "main": [
        [
          {
            "node": "Match Accounts by Account ID",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Fetch Internal Balances": {
      "main": [
        [
          {
            "node": "Edit Internal Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Custodian Balances": {
      "main": [
        [
          {
            "node": "Edit Custodian Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Exception Record": {
      "main": [
        [
          {
            "node": "Append The Data In The Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Balance Mismatch": {
      "main": [
        [
          {
            "node": "Generate AI Mismatch Explanation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Matched Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Balance Difference": {
      "main": [
        [
          {
            "node": "Check for Balance Mismatch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Accounts by Account ID": {
      "main": [
        [
          {
            "node": "Calculate Balance Difference",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Reconciliation on Schedule": {
      "main": [
        [
          {
            "node": "Fetch Internal Balances",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Custodian Balances",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI Mismatch Explanation": {
      "main": [
        [
          {
            "node": "Prepare Exception Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}