{
  "id": "f68GRq87DoenafLK",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Commodity Portfolio Allocation Tracker with Rebalancing Alerts",
  "tags": [],
  "nodes": [
    {
      "id": "7292b81c-f3fb-4b68-abf6-f3a66e703167",
      "name": "Overview Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3424,
        3728
      ],
      "parameters": {
        "width": 1102,
        "height": 316,
        "content": "## Commodity Portfolio Tracker\n\n**How it works**  \nThis workflow runs on a schedule, reads your holdings from Google Sheets, loads target allocation rules from the workflow config node, validates the data, calculates actual allocation, detects drift against your allowed range, classifies the alert severity, generates a plain-text rebalance message with Gemini, emails the result and logs the workflow outcome.\n\n**Setup steps**  \n1. Connect Google Sheets, Gmail and Gemini credentials.  \n2. Confirm the holdings sheet and spreadsheet are correct.  \n3. Create a `rebalance_log` sheet with log columns.  \n4. Update target allocation, alert email and thresholds in the config node.  \n5. Test the workflow once manually, then activate the schedule."
      },
      "typeVersion": 1
    },
    {
      "id": "3a83644b-da4f-4f5c-b6be-d51da74b4af3",
      "name": "Input and Settings Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3424,
        4112
      ],
      "parameters": {
        "color": 7,
        "width": 932,
        "height": 424,
        "content": "## Input and Settings\n\nThis section pulls the latest holdings from Google Sheets and loads the portfolio rules used in the workflow. The settings node acts like the control panel, where you can update target allocation, allowed ranges, alert email, currency and severity rules without changing the rest of the flow."
      },
      "typeVersion": 1
    },
    {
      "id": "233431cb-acd0-41bb-8fe6-9410238a2bf6",
      "name": "Analysis and Decision Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4416,
        4096
      ],
      "parameters": {
        "color": 7,
        "width": 1076,
        "height": 584,
        "content": "## Analysis and Decision\n\nThis part checks whether the input data is clean and usable before doing any calculations. It then works out the actual allocation for each asset, compares it with the target range, identifies which assets are overweight or underweight and decides whether a rebalance alert is needed."
      },
      "typeVersion": 1
    },
    {
      "id": "8527d92a-918b-4e5c-af4f-86b0f81508c6",
      "name": "Alert Message and Logging Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5552,
        3904
      ],
      "parameters": {
        "color": 7,
        "width": 1304,
        "height": 616,
        "content": "## Alert Message and Logging\n\nOnce drift is found, this section turns the result into a clear alert message. Gemini helps format the recommendation in simple language, the email node sends it to the selected recipient and the final step stores the outcome in the log sheet so you have a record of what happened in each run."
      },
      "typeVersion": 1
    },
    {
      "id": "7dd2c230-3740-4a49-a914-026636a254eb",
      "name": "Read Holdings",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3760,
        4384
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=0",
          "cachedResultName": "holdings"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit?usp=drivesdk",
          "cachedResultName": "Commodity Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "79ce3c3c-442a-4a68-af7d-1a7eac2e6039",
      "name": "Workflow Settings",
      "type": "n8n-nodes-base.set",
      "position": [
        3760,
        4224
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "{\n  \"targets\": [\n    { \"asset\": \"Gold\", \"target_pct\": 40, \"min_pct\": 35, \"max_pct\": 45 },\n    { \"asset\": \"Silver\", \"target_pct\": 20, \"min_pct\": 15, \"max_pct\": 25 },\n    { \"asset\": \"Oil\", \"target_pct\": 20, \"min_pct\": 15, \"max_pct\": 25 },\n    { \"asset\": \"Copper\", \"target_pct\": 10, \"min_pct\": 5, \"max_pct\": 15 },\n    { \"asset\": \"Natural Gas\", \"target_pct\": 10, \"min_pct\": 5, \"max_pct\": 15 }\n  ],\n  \"threshold_pct\": 5,\n  \"cooldown_days\": 3,\n  \"alert_email\": \"user@example.com\",\n  \"email_enabled\": \"yes\",\n  \"ai_enabled\": \"yes\",\n  \"portfolio_name\": \"Commodity Portfolio\",\n  \"currency\": \"INR\",\n  \"rebalance_mode\": \"amount\",\n  \"severity_rules\": {\n    \"low_max\": 5,\n    \"medium_max\": 10\n  }\n}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "7d538dfb-8097-4512-b38a-80bd479d671a",
      "name": "Synchronize Inputs",
      "type": "n8n-nodes-base.merge",
      "position": [
        4048,
        4352
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "c0746678-d2e6-4459-8365-9c20c86a7373",
      "name": "Prepare Portfolio Context",
      "type": "n8n-nodes-base.code",
      "position": [
        4240,
        4352
      ],
      "parameters": {
        "jsCode": "const holdings = $('Read Holdings').all().map(i => i.json);\nconst config = $('Workflow Settings').first().json;\n\nreturn [\n  {\n    json: {\n      holdings,\n      config\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "91c40090-c446-45d2-ac33-f9e4c3bec82c",
      "name": "Validate Portfolio Data",
      "type": "n8n-nodes-base.code",
      "position": [
        4464,
        4352
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst holdings = data.holdings || [];\nconst config = data.config || {};\nconst targets = config.targets || [];\n\nif (!holdings.length) {\n  return [{\n    json: {\n      valid: false,\n      error: 'No holdings found.',\n      holdings,\n      config\n    }\n  }];\n}\n\nif (!targets.length) {\n  return [{\n    json: {\n      valid: false,\n      error: 'Targets are missing in workflow settings.',\n      holdings,\n      config\n    }\n  }];\n}\n\nconst targetTotal = targets.reduce((sum, t) => sum + Number(t.target_pct || 0), 0);\nif (targetTotal !== 100) {\n  return [{\n    json: {\n      valid: false,\n      error: `Target allocation must total 100. Current total is ${targetTotal}.`,\n      holdings,\n      config\n    }\n  }];\n}\n\nconst seenAssets = new Set();\n\nfor (const h of holdings) {\n  const asset = String(h.asset || '').trim();\n  const units = Number(h.units);\n  const price = Number(h.price);\n\n  if (!asset) {\n    return [{\n      json: {\n        valid: false,\n        error: 'One holding has a blank asset name.',\n        holdings,\n        config\n      }\n    }];\n  }\n\n  if (seenAssets.has(asset)) {\n    return [{\n      json: {\n        valid: false,\n        error: `Duplicate asset found in holdings: ${asset}`,\n        holdings,\n        config\n      }\n    }];\n  }\n  seenAssets.add(asset);\n\n  if (Number.isNaN(units) || units < 0) {\n    return [{\n      json: {\n        valid: false,\n        error: `Invalid units for asset: ${asset}`,\n        holdings,\n        config\n      }\n    }];\n  }\n\n  if (Number.isNaN(price) || price < 0) {\n    return [{\n      json: {\n        valid: false,\n        error: `Invalid price for asset: ${asset}`,\n        holdings,\n        config\n      }\n    }];\n  }\n\n  const target = targets.find(t => String(t.asset || '').trim() === asset);\n  if (!target) {\n    return [{\n      json: {\n        valid: false,\n        error: `No target config found for asset: ${asset}`,\n        holdings,\n        config\n      }\n    }];\n  }\n}\n\nreturn [{\n  json: {\n    valid: true,\n    error: null,\n    holdings,\n    config\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f66778ac-7cb4-4ce7-b4cc-81e08796c440",
      "name": "Check Validation Status",
      "type": "n8n-nodes-base.if",
      "position": [
        4704,
        4352
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.valid }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "52d2ef3f-d231-49b2-b7b2-e134326fe6a4",
      "name": "Calculate Portfolio Allocation",
      "type": "n8n-nodes-base.code",
      "position": [
        4928,
        4240
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst holdings = data.holdings;\nconst config = data.config;\n\nconst enrichedHoldings = holdings.map(h => {\n  const units = Number(h.units);\n  const price = Number(h.price);\n  const current_value = Number(h.current_value ?? (units * price));\n\n  return {\n    row_number: h.row_number,\n    asset: h.asset,\n    units,\n    price,\n    current_value,\n    last_updated: h.last_updated\n  };\n});\n\nconst total_value = enrichedHoldings.reduce((sum, h) => sum + h.current_value, 0);\n\nconst assets = enrichedHoldings.map(h => {\n  const target = config.targets.find(t => t.asset === h.asset);\n\n  if (!target) {\n    throw new Error(`Missing target config for asset: ${h.asset}`);\n  }\n\n  const actual_pct = total_value === 0 ? 0 : (h.current_value / total_value) * 100;\n\n  return {\n    ...h,\n    actual_pct,\n    target_pct: Number(target.target_pct),\n    min_pct: Number(target.min_pct),\n    max_pct: Number(target.max_pct)\n  };\n});\n\nreturn [{\n  json: {\n    holdings: assets,\n    total_value,\n    config\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "15978e61-2607-442d-9004-63d5aa829bb8",
      "name": "Detect Portfolio Drift",
      "type": "n8n-nodes-base.code",
      "position": [
        5136,
        4240
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst holdings = data.holdings;\nconst total_value = data.total_value;\nconst config = data.config;\n\nlet rebalance_needed = false;\n\nconst assets = holdings.map(h => {\n  const deviation_pct = h.actual_pct - h.target_pct;\n  const target_value = total_value * (h.target_pct / 100);\n  const adjustment_amount = Math.abs(h.current_value - target_value);\n\n  let status = 'within_range';\n  let suggested_action = 'Hold';\n\n  if (h.actual_pct > h.max_pct) {\n    status = 'overweight';\n    suggested_action = 'Reduce';\n    rebalance_needed = true;\n  } else if (h.actual_pct < h.min_pct) {\n    status = 'underweight';\n    suggested_action = 'Increase';\n    rebalance_needed = true;\n  }\n\n  return {\n    ...h,\n    deviation_pct,\n    target_value,\n    adjustment_amount,\n    status,\n    suggested_action\n  };\n});\n\nconst affected_assets = assets\n  .filter(a => a.status !== 'within_range')\n  .map(a => a.asset);\n\nreturn [{\n  json: {\n    total_value,\n    assets,\n    affected_assets,\n    rebalance_needed,\n    config\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3d0e7dce-1d90-44c7-8751-5c83c27b46bb",
      "name": "Check Rebalance Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        5360,
        4240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.rebalance_needed }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ec658cb8-383f-43c4-8c95-3717f711a94e",
      "name": "Classify Alert Severity",
      "type": "n8n-nodes-base.code",
      "position": [
        5584,
        4144
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst assets = data.assets;\nconst config = data.config;\n\nconst maxDeviation = Math.max(...assets.map(a => Math.abs(a.deviation_pct)));\n\nlet severity = 'low';\nif (maxDeviation > Number(config.severity_rules.medium_max)) {\n  severity = 'high';\n} else if (maxDeviation > Number(config.severity_rules.low_max)) {\n  severity = 'medium';\n}\n\nreturn [{\n  json: {\n    ...data,\n    severity,\n    max_deviation: maxDeviation\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "622f0d6d-17b4-4ec0-ab5f-1f67ddd90b95",
      "name": "Build Alert Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        5808,
        4144
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\nconst affected = data.assets\n  .filter(a => a.status !== 'within_range')\n  .map(a => `${a.asset}: ${a.status}, actual ${a.actual_pct.toFixed(2)}%, target ${a.target_pct.toFixed(2)}%, deviation ${a.deviation_pct.toFixed(2)}%, action ${a.suggested_action}, amount ${data.config.currency} ${a.adjustment_amount.toFixed(2)}`)\n  .join('\\n');\n\nconst ai_input = `\nYou are a financial portfolio monitoring assistant.\n\nYour task is to convert structured portfolio rebalance data into a professional alert message.\n\nSTRICT RULES:\n- Use only the numbers provided below\n- Do NOT recalculate anything\n- Do NOT invent any values\n- Keep the message concise, clear and professional\n- Use plain text only\n- Mention severity naturally\n\nDATA:\nPortfolio Name: ${data.config.portfolio_name}\nCurrency: ${data.config.currency}\nTotal Portfolio Value: ${data.total_value}\nSeverity: ${data.severity}\nMaximum Deviation: ${data.max_deviation.toFixed(2)}%\n\nAssets requiring rebalance:\n${affected}\n\nOUTPUT FORMAT:\nWrite a clean paragraph-style alert message suitable for email.\n`;\n\nreturn [{\n  json: {\n    ...data,\n    ai_input\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9379e3af-ce16-481c-9def-ca9541d41667",
      "name": "Generate Alert Message",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        5952,
        4064
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3.1-flash-lite-preview",
          "cachedResultName": "models/gemini-3.1-flash-lite-preview"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "={{ $json.ai_input }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "77a77ce0-4569-4c61-bb8c-cf7a1d885dcd",
      "name": "Merge Alert Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        6256,
        4128
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "a8d39754-f8b5-4cab-a8f2-819ceec55b46",
      "name": "Prepare Alert Payload",
      "type": "n8n-nodes-base.set",
      "position": [
        6432,
        4128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "3de685c8-ff1f-43a1-870d-e58542b7596b",
              "name": "alert_message",
              "type": "string",
              "value": "={{ $json.content.parts[0].text }}"
            },
            {
              "id": "fbc8f85a-24b4-40d0-9981-e4b85f38c6a1",
              "name": "email_to",
              "type": "string",
              "value": "={{ $json.config.alert_email }}"
            },
            {
              "id": "dc6b09bd-6745-4110-8ec4-2322014da2a1",
              "name": "email_subject",
              "type": "string",
              "value": "=Commodity Portfolio Alert - {{$json.severity.toUpperCase()}}"
            },
            {
              "id": "c715d84c-f0c7-47c5-a3ef-2dd0c2bccbd6",
              "name": "affected_assets_text",
              "type": "string",
              "value": "={{ $json.affected_assets.join(', ') }}"
            },
            {
              "id": "f4861430-940f-48b2-be93-2d810c5ada97",
              "name": "run_date",
              "type": "string",
              "value": "={{ $now }}"
            },
            {
              "id": "aef321d5-2581-43e2-85db-42cdb7515f16",
              "name": "alert_sent",
              "type": "string",
              "value": "yes"
            },
            {
              "id": "43b07038-8485-4efa-9c06-7d0eee590f5e",
              "name": "total_value",
              "type": "number",
              "value": "={{ $json.total_value }}"
            },
            {
              "id": "24388c93-02d0-4fa8-b84f-83a7f1fca40b",
              "name": "severity",
              "type": "string",
              "value": "={{ $json.severity }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "708ee600-4e9a-4265-bbf1-3c105dbf080f",
      "name": "Send Rebalance Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        6608,
        4048
      ],
      "parameters": {
        "sendTo": "={{ $('Prepare Alert Payload').item.json.email_to }}",
        "message": "={{ $('Prepare Alert Payload').item.json.alert_message + '\\n\\nAffected Assets: ' + $('Prepare Alert Payload').item.json.affected_assets_text + '\\nSeverity: ' + $('Prepare Alert Payload').item.json.severity + '\\nTotal Portfolio Value: ' + $('Prepare Alert Payload').item.json.total_value + '\\nRun Date: ' + $('Prepare Alert Payload').item.json.run_date }}",
        "options": {},
        "subject": "={{ $('Prepare Alert Payload').item.json.email_subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f6e4ac7b-b17d-4d3e-91f4-4656880e1a4a",
      "name": "Log Sent Alert",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        6608,
        4224
      ],
      "parameters": {
        "columns": {
          "value": {
            "summary": "={{ $json.alert_message }}",
            "run_date": "={{ $json.run_date }}",
            "severity": "={{ $json.severity }}",
            "alert_sent": "={{ $json.alert_sent }}",
            "total_value": "={{ $json.total_value }}",
            "affected_assets": "={{ $json.affected_assets_text }}",
            "rebalance_needed": "yes"
          },
          "schema": [
            {
              "id": "run_date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "run_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_value",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_value",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rebalance_needed",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rebalance_needed",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "severity",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "severity",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "affected_assets",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "affected_assets",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "alert_sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "alert_sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reason_skipped",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reason_skipped",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 347522276,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=347522276",
          "cachedResultName": "rebalance_log"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
          "cachedResultName": "Commodity Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "91883dee-8be2-4ed6-93a8-802dda291517",
      "name": "Prepare Validation Failure Log",
      "type": "n8n-nodes-base.set",
      "position": [
        4928,
        4512
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "run_date",
              "type": "string",
              "value": "={{ $now }}"
            },
            {
              "name": "total_value",
              "type": "string",
              "value": ""
            },
            {
              "name": "rebalance_needed",
              "type": "string",
              "value": "no"
            },
            {
              "name": "severity",
              "type": "string",
              "value": "validation_failed"
            },
            {
              "name": "affected_assets",
              "type": "string",
              "value": ""
            },
            {
              "name": "alert_sent",
              "type": "string",
              "value": "no"
            },
            {
              "name": "summary",
              "type": "string",
              "value": "Validation failed before allocation analysis."
            },
            {
              "name": "reason_skipped",
              "type": "string",
              "value": "={{ $json.error }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "5300f585-5b9a-42c8-88ca-b3cb8766ac80",
      "name": "Log Validation Failure",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        5136,
        4512
      ],
      "parameters": {
        "columns": {
          "value": {
            "summary": "={{ $json.summary }}",
            "run_date": "={{ $json.run_date }}",
            "severity": "={{ $json.severity }}",
            "alert_sent": "={{ $json.alert_sent }}",
            "total_value": "={{ $json.total_value }}",
            "reason_skipped": "={{ $json.reason_skipped }}",
            "affected_assets": "={{ $json.affected_assets }}",
            "rebalance_needed": "={{ $json.rebalance_needed }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 347522276,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=347522276",
          "cachedResultName": "rebalance_log"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
          "cachedResultName": "Commodity Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "c899fa73-de9a-4814-ab75-71489c8da0d2",
      "name": "Prepare No Action Log",
      "type": "n8n-nodes-base.set",
      "position": [
        5584,
        4304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "run_date",
              "type": "string",
              "value": "={{ $now }}"
            },
            {
              "name": "total_value",
              "type": "string",
              "value": "={{ $json.total_value }}"
            },
            {
              "name": "rebalance_needed",
              "type": "string",
              "value": "no"
            },
            {
              "name": "severity",
              "type": "string",
              "value": "none"
            },
            {
              "name": "affected_assets",
              "type": "string",
              "value": ""
            },
            {
              "name": "alert_sent",
              "type": "string",
              "value": "no"
            },
            {
              "name": "summary",
              "type": "string",
              "value": "No drift detected. Portfolio is within target range."
            },
            {
              "name": "reason_skipped",
              "type": "string",
              "value": "No rebalance needed."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e62048b2-5b87-4166-84cb-25109dcdfdfd",
      "name": "Log No Action",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        5808,
        4304
      ],
      "parameters": {
        "columns": {
          "value": {
            "summary": "={{ $json.summary }}",
            "run_date": "={{ $json.run_date }}",
            "severity": "={{ $json.severity }}",
            "alert_sent": "={{ $json.alert_sent }}",
            "total_value": "={{ $json.total_value }}",
            "reason_skipped": "={{ $json.reason_skipped }}",
            "affected_assets": "={{ $json.affected_assets }}",
            "rebalance_needed": "={{ $json.rebalance_needed }}"
          },
          "schema": [
            {
              "id": "run_date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "run_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_value",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "total_value",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rebalance_needed",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rebalance_needed",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "severity",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "severity",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "affected_assets",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "affected_assets",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "alert_sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "alert_sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reason_skipped",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reason_skipped",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 347522276,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA/edit#gid=347522276",
          "cachedResultName": "rebalance_log"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1_lcrJ7b0c9ZqyenXZZZ-A5Yyb99lS-k8qSs-Y9PV3EA",
          "cachedResultName": "Commodity Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "0695d34f-1ed2-4f48-8d5b-07fd7ae07158",
      "name": "Daily Portfolio Rebalance Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        3488,
        4352
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "8bcf8ba9-03c0-47a1-b028-912e88818797",
  "connections": {
    "Read Holdings": {
      "main": [
        [
          {
            "node": "Synchronize Inputs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Log Sent Alert": {
      "main": [
        []
      ]
    },
    "Merge Alert Data": {
      "main": [
        [
          {
            "node": "Prepare Alert Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Settings": {
      "main": [
        [
          {
            "node": "Synchronize Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Alert Prompt": {
      "main": [
        [
          {
            "node": "Generate Alert Message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Alert Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Synchronize Inputs": {
      "main": [
        [
          {
            "node": "Prepare Portfolio Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Rebalance Email": {
      "main": [
        []
      ]
    },
    "Prepare Alert Payload": {
      "main": [
        [
          {
            "node": "Log Sent Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Rebalance Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare No Action Log": {
      "main": [
        [
          {
            "node": "Log No Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Portfolio Drift": {
      "main": [
        [
          {
            "node": "Check Rebalance Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Alert Message": {
      "main": [
        [
          {
            "node": "Merge Alert Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Validation Status": {
      "main": [
        [
          {
            "node": "Calculate Portfolio Allocation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Validation Failure Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Alert Severity": {
      "main": [
        [
          {
            "node": "Build Alert Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Portfolio Data": {
      "main": [
        [
          {
            "node": "Check Validation Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Portfolio Context": {
      "main": [
        [
          {
            "node": "Validate Portfolio Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Rebalance Requirement": {
      "main": [
        [
          {
            "node": "Classify Alert Severity",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare No Action Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Portfolio Allocation": {
      "main": [
        [
          {
            "node": "Detect Portfolio Drift",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Validation Failure Log": {
      "main": [
        [
          {
            "node": "Log Validation Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Portfolio Rebalance Check": {
      "main": [
        [
          {
            "node": "Read Holdings",
            "type": "main",
            "index": 0
          },
          {
            "node": "Workflow Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}