{
  "id": "WEJP9WvDDXUMfZec",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Corporate Actions Impact Analyzer",
  "tags": [],
  "nodes": [
    {
      "id": "baa35d30-c955-4922-98d4-19665b32d954",
      "name": "Run Workflow (Scheduler)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        3968,
        2512
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "4de30f83-409c-4751-bf19-1b30624bcdbf",
      "name": "Config (Edit Fields)",
      "type": "n8n-nodes-base.set",
      "position": [
        4144,
        2512
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5fbe6e73-3828-43d7-9357-e3e503a3e749",
              "name": "client_id",
              "type": "string",
              "value": "CLIENT_001"
            },
            {
              "id": "5a224ef8-22f8-4161-8747-d25b0910165c",
              "name": "admin_email",
              "type": "string",
              "value": "\"CHANGE_ME\""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "367587e7-e212-410c-a784-e9e28023628d",
      "name": "Normalize RSS",
      "type": "n8n-nodes-base.code",
      "position": [
        4560,
        2512
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nfunction cleanText(v) {\n  return (v || '').toString().replace(/\\s+/g, ' ').trim();\n}\n\nfunction extractField(text, fieldName) {\n  const regex = new RegExp(`${fieldName}\\\\s*:\\\\s*([^|]+)`, 'i');\n  const match = text.match(regex);\n  return match ? cleanText(match[1]) : null;\n}\n\nfunction detectActionType(purpose) {\n  const p = (purpose || '').toUpperCase();\n  if (p.includes('INTERIM DIVIDEND') || p.includes('DIVIDEND')) return 'dividend';\n  if (p.includes('BONUS')) return 'bonus';\n  if (p.includes('SPLIT')) return 'split';\n  if (p.includes('RIGHTS')) return 'rights';\n  if (p.includes('BUYBACK')) return 'buyback';\n  if (p.includes('MERGER')) return 'merger';\n  if (p.includes('DEMERGER')) return 'demerger';\n  return 'other';\n}\n\nfunction extractDividendPerShare(purpose) {\n  if (!purpose) return null;\n  const match = purpose.match(/(?:RS|RE)\\s*\\.?\\s*([0-9]+(?:\\.[0-9]+)?)/i);\n  return match ? Number(match[1]) : null;\n}\n\nfunction extractRatio(purpose) {\n  if (!purpose) return { ratio_text: null, ratio_num: null, ratio_den: null };\n  const match = purpose.match(/(\\d+)\\s*:\\s*(\\d+)/);\n  if (!match) return { ratio_text: null, ratio_num: null, ratio_den: null };\n\n  return {\n    ratio_text: `${match[1]}:${match[2]}`,\n    ratio_num: Number(match[1]),\n    ratio_den: Number(match[2]),\n  };\n}\n\nconst rssItems = items.map(({ json: row }) => {\n  const title = cleanText(row.title);\n  const content = cleanText(row.content || row.contentSnippet || '');\n  const link = cleanText(row.link);\n  const pubDate = cleanText(row.pubDate);\n  const isoDate = cleanText(row.isoDate);\n\n  const titleMatch = title.match(/^(.*?)\\s*-\\s*Ex-Date:\\s*(.*)$/i);\n  const company_name = titleMatch ? cleanText(titleMatch[1]) : title;\n  const ex_date = titleMatch ? cleanText(titleMatch[2]) : null;\n\n  const purpose = extractField(content, 'PURPOSE');\n  const face_value = extractField(content, 'FACE VALUE');\n  const record_date = extractField(content, 'RECORD DATE');\n  const series = extractField(content, 'SERIES');\n  const action_type = detectActionType(purpose);\n  const dividend_per_share = extractDividendPerShare(purpose);\n  const ratio = extractRatio(purpose);\n\n  return {\n    source: 'nse_rss',\n    source_priority: 100,\n    source_url: link,\n    company_name,\n    raw_title: title,\n    raw_content: content,\n    pubDate,\n    isoDate,\n    ex_date,\n    record_date,\n    face_value: face_value ? Number(face_value) || face_value : null,\n    series,\n    raw_purpose: purpose,\n    action_type,\n    dividend_per_share,\n    ratio_text: ratio.ratio_text,\n    ratio_num: ratio.ratio_num,\n    ratio_den: ratio.ratio_den,\n  };\n});\n\nreturn [\n  {\n    json: {\n      rssItems,\n      rss_count: rssItems.length\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "71cf61f9-27c2-4dd6-ab40-77c4f3b21806",
      "name": "Fetch Portfolio (Single Client)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4784,
        2512
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit#gid=0",
          "cachedResultName": "Portfolio Sheet"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit?usp=drivesdk",
          "cachedResultName": "Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "0f7e67d3-b792-4bd3-acd6-bf4b87e68bbc",
      "name": "Compare Holdings + Build Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        5024,
        2512
      ],
      "parameters": {
        "jsCode": "const config = $('Config (Edit Fields)').first().json || {};\nconst rssItems = $('Normalize RSS').first().json.rssItems || [];\nconst holdings = $input.all().map(x => x.json);\n\nfunction normalizeName(name) {\n  return (name || '')\n    .toString()\n    .toLowerCase()\n    .replace(/limited/g, '')\n    .replace(/\\bltd\\b/g, '')\n    .replace(/company/g, '')\n    .replace(/corporation/g, '')\n    .replace(/industries/g, 'industry')\n    .replace(/[^a-z0-9]/g, '')\n    .trim();\n}\n\nconst configuredClientId = (config.client_id || '').toString().trim();\n\nconst filteredHoldings = configuredClientId\n  ? holdings.filter(h => (h.client_id || '').toString().trim() === configuredClientId)\n  : holdings;\n\nconst matches = [];\nconst seen = new Set();\n\nfor (const holding of filteredHoldings) {\n  const holdingName = normalizeName(holding.asset_name || holding.company_name || '');\n\n  for (const rss of rssItems) {\n    const rssName = normalizeName(rss.company_name || '');\n    const isMatch = holdingName && rssName && holdingName === rssName;\n\n    if (!isMatch) continue;\n\n    const dedupeKey = [\n      holding.client_id || '',\n      holding.symbol || '',\n      rss.company_name || '',\n      rss.action_type || '',\n      rss.record_date || '',\n      rss.ex_date || ''\n    ].join('|');\n\n    if (seen.has(dedupeKey)) continue;\n    seen.add(dedupeKey);\n\n    matches.push({\n      client_id: holding.client_id || null,\n      client_name: holding.client_name || null,\n      email: holding.email || null,\n      holding_symbol: holding.symbol || null,\n      holding_company_name: holding.asset_name || holding.company_name || null,\n      quantity: Number(holding.quantity) || 0,\n      avg_buy_price: Number(holding.avg_buy_price) || 0,\n      asset_type: holding.asset_type || null,\n      ...rss\n    });\n  }\n}\n\nconst first = filteredHoldings[0] || {};\n\nconst prompt = `\nYou are a financial corporate action assistant.\n\nSTRICT OUTPUT RULES:\n- Return ONLY valid JSON\n- DO NOT use markdown\n- DO NOT wrap response in backticks\n- DO NOT add explanations before or after JSON\n- Response must start with { and end with }\n- Use only the provided data\n- Do not invent missing values\n\nCalculation rules:\n- For dividend: estimated_cash_benefit = holding_quantity \u00d7 dividend_per_share\n- For bonus: estimated_new_shares = floor(holding_quantity \u00d7 ratio_num / ratio_den) when ratio is available\n- For split: explain the likely unit impact in simple words; set estimated_new_shares to 0 unless clearly derivable\n- For rights: if details are incomplete, set needs_review = true\n- For merger/demerger/buyback/other with incomplete details: set needs_review = true\n\nReturn JSON in exactly this structure:\n{\n  \"client_id\": \"string\",\n  \"client_name\": \"string\",\n  \"email\": \"string|null\",\n  \"total_matches\": 0,\n  \"actions\": [\n    {\n      \"company_name\": \"string\",\n      \"action_type\": \"string\",\n      \"holding_quantity\": 0,\n      \"estimated_cash_benefit\": 0,\n      \"estimated_new_shares\": 0,\n      \"action_needed\": \"string\",\n      \"summary\": \"string\",\n      \"needs_review\": false\n    }\n  ],\n  \"overall_summary\": \"string\"\n}\n\nInput data:\n${JSON.stringify(matches, null, 2)}\n`;\n\nreturn [\n  {\n    json: {\n      has_matches: matches.length > 0,\n      configured_client_id: configuredClientId || null,\n      client_id: first.client_id || configuredClientId || 'CLIENT_001',\n      client_name: first.client_name || 'Sample Client',\n      email: first.email || 'user@example.com',\n      rss_count: rssItems.length,\n      holdings_count: filteredHoldings.length,\n      total_matches: matches.length,\n      matches,\n      prompt\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ce7145b1-8c67-49ee-af4f-b48d774a682e",
      "name": "Has Matching Actions?",
      "type": "n8n-nodes-base.if",
      "position": [
        5264,
        2512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-matches-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.has_matches }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "bab51b75-9009-45c5-aa3f-4b600dd46e73",
      "name": "Generate AI Impact Summary",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        5456,
        2432
      ],
      "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": [
            {
              "role": "=user",
              "content": "={{ $json.prompt }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "a6a9b26a-a2cd-48ad-872d-377ef64c0809",
      "name": "Clean + Parse AI Output",
      "type": "n8n-nodes-base.code",
      "position": [
        5744,
        2432
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nlet raw = '';\n\nif (typeof item.content?.parts?.[0]?.text === 'string') {\n  raw = item.content.parts[0].text;\n} else if (typeof item.parts?.[0]?.text === 'string') {\n  raw = item.parts[0].text;\n} else if (typeof item.text === 'string') {\n  raw = item.text;\n} else if (typeof item.content === 'string') {\n  raw = item.content;\n} else if (typeof item.output === 'string') {\n  raw = item.output;\n} else if (typeof item.response === 'string') {\n  raw = item.response;\n} else if (typeof item.message?.content === 'string') {\n  raw = item.message.content;\n} else if (typeof item.candidates?.[0]?.content?.parts?.[0]?.text === 'string') {\n  raw = item.candidates[0].content.parts[0].text;\n}\n\nlet cleaned = raw;\n\nif (typeof cleaned === 'string') {\n  cleaned = cleaned.trim()\n    .replace(/^```json\\s*/i, '')\n    .replace(/^```\\s*/i, '')\n    .replace(/\\s*```$/i, '')\n    .trim();\n}\n\nif (!cleaned) {\n  return [{ json: { parse_error: true, error_message: 'No JSON text found in model output', raw_ai_output: item } }];\n}\n\ntry {\n  const parsed = JSON.parse(cleaned);\n  return [{ json: { ...parsed, parse_error: false } }];\n} catch (e) {\n  return [{\n    json: {\n      parse_error: true,\n      error_message: e instanceof Error ? e.message : String(e),\n      raw_ai_output: raw,\n      cleaned_output: cleaned\n    }\n  }];\n}\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a28866a9-04b7-4d30-b18c-434f027940d6",
      "name": "AI Output Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        5984,
        2432
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "parse-error-check",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.parse_error }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c6a2fac3-4b60-44d2-8be6-216b7eb80286",
      "name": "Format Email + Log Row",
      "type": "n8n-nodes-base.code",
      "position": [
        6224,
        2352
      ],
      "parameters": {
        "jsCode": "const data = $json;\nconst actions = Array.isArray(data.actions) ? data.actions : [];\n\nconst subject = `Corporate Actions Summary - ${data.client_name}`;\n\nconst actionLines = actions.map((action, index) => {\n  return `${index + 1}. ${action.company_name}\n- Action Type: ${action.action_type}\n- Holding Quantity: ${action.holding_quantity}\n- Estimated Cash Benefit: Rs ${action.estimated_cash_benefit}\n- Estimated New Shares: ${action.estimated_new_shares}\n- Action Needed: ${action.action_needed}\n- Summary: ${action.summary}`;\n}).join('\\n\\n');\n\nconst body = `Dear ${data.client_name},\n\nPlease find below the latest corporate actions matched to your portfolio.\n\nTotal Matched Actions: ${data.total_matches}\n\nOverall Summary:\n${data.overall_summary}\n\nDetailed Breakdown:\n${actionLines}\n\nThis is an automated portfolio update based on currently matched corporate action announcements.\n\nRegards,\nPortfolio Assistant`;\n\nreturn [{\n  json: {\n    client_id: data.client_id,\n    client_name: data.client_name,\n    email: data.email,\n    total_matches: data.total_matches,\n    overall_summary: data.overall_summary,\n    actions_json: JSON.stringify(actions),\n    status: 'matched_actions_sent',\n    subject,\n    body,\n    logged_at: new Date().toISOString()\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c61e0c16-850e-459d-98b5-915c061f3b47",
      "name": "Send Email Notification",
      "type": "n8n-nodes-base.gmail",
      "position": [
        6464,
        2352
      ],
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "message": "={{ $json.body }}",
        "options": {},
        "subject": "={{ $json.subject }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1c9a857a-62d8-404c-b4aa-4da54d9c3876",
      "name": "Log Result to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        6704,
        2352
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 41317953,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit#gid=41317953",
          "cachedResultName": "Action_Log Sheet"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit?usp=drivesdk",
          "cachedResultName": "Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "8feec488-22e8-42ea-9e0d-bbe5b6396570",
      "name": "Prepare No-Match Output",
      "type": "n8n-nodes-base.code",
      "position": [
        5424,
        2704
      ],
      "parameters": {
        "jsCode": "const input = $json;\n\nreturn [{\n  json: {\n    logged_at: new Date().toISOString(),\n    client_id: input.client_id,\n    client_name: input.client_name,\n    email: input.email,\n    status: 'no_matching_actions',\n    total_matches: 0,\n    overall_summary: 'No matching corporate actions were found for this client in the latest NSE RSS run.',\n    actions_json: '[]',\n    subject: `Corporate Actions Check - ${input.client_name}`,\n    body: `Dear ${input.client_name},\n\nWe checked the latest NSE corporate action RSS feed and found no matching actions for your current portfolio in this run.\n\nRegards,\nPortfolio Assistant`\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e8d6d5fe-feb9-4a0d-91fb-9c32750ca84f",
      "name": "Send No-Match Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        5600,
        2704
      ],
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "message": "={{ $json.body }}",
        "options": {},
        "subject": "={{ $json.subject }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "288443da-13ac-4982-8a31-942acbf872b1",
      "name": "Log No-Match Run",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        5776,
        2704
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 41317953,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit#gid=41317953",
          "cachedResultName": "Action_Log Sheet"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit?usp=drivesdk",
          "cachedResultName": "Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "9c91dbb9-09a8-441d-8911-52e04ea91475",
      "name": "Prepare Parse-Error Output",
      "type": "n8n-nodes-base.code",
      "position": [
        6224,
        2576
      ],
      "parameters": {
        "jsCode": "const item = $('Clean + Parse AI Output').first().json;\nconst base = $('Compare Holdings + Build Prompt').first().json;\nconst config = $('Config (Edit Fields)').first().json || {};\n\nreturn [{\n  json: {\n    logged_at: new Date().toISOString(),\n    client_id: base.client_id,\n    client_name: base.client_name,\n    email: base.email,\n    admin_email: config.admin_email || 'admin@example.com',\n    status: 'ai_parse_error',\n    total_matches: base.total_matches,\n    overall_summary: 'AI returned an invalid response and needs review.',\n    actions_json: '[]',\n    subject: `Corporate Actions Review Needed - ${base.client_name}`,\n    body: `Dear Admin,\n\nThe workflow found matching corporate actions for ${base.client_name}, but the AI response could not be parsed.\n\nError: ${item.error_message || 'Unknown parsing error'}\n\nPlease review the run in n8n.`,\n    parse_error_details: JSON.stringify(item)\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fb9fe75c-cd69-4c28-920c-04db9aced500",
      "name": "Send Admin Review Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        6464,
        2576
      ],
      "parameters": {
        "sendTo": "={{ $json.admin_email || $('Config (Edit Fields)').first().json.admin_email }}",
        "message": "={{ $json.body }}",
        "options": {},
        "subject": "={{ $json.subject }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5680d124-6c96-455f-8d36-2257cfae347f",
      "name": "Log Parse Error",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        6704,
        2576
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 41317953,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit#gid=41317953",
          "cachedResultName": "Action_Log Sheet"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/11MztuG2MGuyyD2Ek79sviW_y_XAWvWXmvItUTChEdy4/edit?usp=drivesdk",
          "cachedResultName": "Portfolio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "867cf89c-014f-4818-b743-98c268963181",
      "name": "Overview Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3936,
        1952
      ],
      "parameters": {
        "width": 1120,
        "height": 340,
        "content": "## Single-Client Corporate Actions Analyzer\n\n**How it works**  \nThis workflow runs on a schedule, reads editable settings from the Config node, fetches the latest NSE corporate action RSS feed, normalizes the feed into structured fields, checks one client\u2019s portfolio from Google Sheets, sends matched actions to Gemini for impact analysis, emails the result and logs every run.\n\n**Setup steps**  \n1. Open `Config (Edit Fields)` and update `client_id` and `admin_email`  \n2. Keep `Portfolio` and `Action_Log` sheet names ready  \n3. Connect Google Sheets, Gmail and Gemini credentials  \n4. Make sure your `Portfolio` sheet has the correct columns  \n5. Test once manually before activating"
      },
      "typeVersion": 1
    },
    {
      "id": "7c8b0033-e258-464b-947d-714cf96e7580",
      "name": "Feed Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3936,
        2368
      ],
      "parameters": {
        "color": 7,
        "width": 748,
        "height": 306,
        "content": "## Feed Processing\n\nGets the NSE RSS feed and converts raw text into structured action fields like company name, action type, dividend amount, ratio and important dates."
      },
      "typeVersion": 1
    },
    {
      "id": "bd7fae2c-8678-4218-bdbd-ccbf6c6c8a4f",
      "name": "AI Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4736,
        2320
      ],
      "parameters": {
        "color": 7,
        "width": 1162,
        "height": 546,
        "content": "## Matching + AI\n\nReads one client from Sheets, matches holdings to corporate actions and asks Gemini to calculate impact in a strict JSON structure."
      },
      "typeVersion": 1
    },
    {
      "id": "718c91fd-f9c6-4c05-9d89-7ac9a1814081",
      "name": "Output Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5952,
        2224
      ],
      "parameters": {
        "color": 7,
        "width": 940,
        "height": 566,
        "content": "## Output + Logging\n\nHandles three cases cleanly: matched actions, no matches and AI parse failure. Each run is emailed and logged to a sheet for auditability."
      },
      "typeVersion": 1
    },
    {
      "id": "17fed412-1d11-447c-98a2-41a78b656a1e",
      "name": "Fetch Corporate Actions",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        4336,
        2512
      ],
      "parameters": {
        "url": "https://nsearchives.nseindia.com/content/RSS/Corporate_action.xml",
        "options": {}
      },
      "typeVersion": 1.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "08feaa48-406a-44d6-bd96-971701b80011",
  "connections": {
    "Normalize RSS": {
      "main": [
        [
          {
            "node": "Fetch Portfolio (Single Client)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Output Valid?": {
      "main": [
        [
          {
            "node": "Format Email + Log Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Parse-Error Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send No-Match Email": {
      "main": [
        [
          {
            "node": "Log No-Match Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config (Edit Fields)": {
      "main": [
        [
          {
            "node": "Fetch Corporate Actions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Matching Actions?": {
      "main": [
        [
          {
            "node": "Generate AI Impact Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare No-Match Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Email + Log Row": {
      "main": [
        [
          {
            "node": "Send Email Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean + Parse AI Output": {
      "main": [
        [
          {
            "node": "AI Output Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Corporate Actions": {
      "main": [
        [
          {
            "node": "Normalize RSS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare No-Match Output": {
      "main": [
        [
          {
            "node": "Send No-Match Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Admin Review Email": {
      "main": [
        [
          {
            "node": "Log Parse Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email Notification": {
      "main": [
        [
          {
            "node": "Log Result to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Workflow (Scheduler)": {
      "main": [
        [
          {
            "node": "Config (Edit Fields)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI Impact Summary": {
      "main": [
        [
          {
            "node": "Clean + Parse AI Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Parse-Error Output": {
      "main": [
        [
          {
            "node": "Send Admin Review Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare Holdings + Build Prompt": {
      "main": [
        [
          {
            "node": "Has Matching Actions?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Portfolio (Single Client)": {
      "main": [
        [
          {
            "node": "Compare Holdings + Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}