This workflow corresponds to n8n.io template #17361 — we link there as the canonical source.
This workflow follows the Agent → Emailsend recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "4mIHotRjUv96W7uM",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Personal Financial Digital Twin",
"tags": [],
"nodes": [
{
"id": "03b935a1-dfcc-4a2d-8c00-715077021f52",
"name": "Sticky Note - Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
0
],
"parameters": {
"width": 1020,
"height": 1012,
"content": "## Personal Financial Digital Twin\n\nThis workflow builds a Monte Carlo \"digital twin\" of the user's real financial life, then stress-tests it against a chosen life event (buy a house, lose a job, or both) using their actual linked account balances and real spending patterns - not hypothetical numbers.\n\n### Who's it for\n- Anyone who wants to know \"can I actually afford this\" before signing a mortgage or before a layoff wave hits their industry\n- Fintech apps or advisors who want a real-accounts-based stress test instead of a generic calculator\n- Personal finance tools that want to move from static budgeting to probabilistic planning\n\n### How it works\n1. A form captures the life event to test (Buy a House / Lose a Job / Both) plus its parameters (home price, down payment %, job-loss duration, simulation horizon)\n2. Two APIs are called: the user's linked account balances & recent transactions (modeled on a Plaid-style aggregator), and historical market return statistics (mean/volatility) to drive realistic random returns\n3. JS - Build Financial Baseline & Simulation Inputs turns raw account/transaction data into a clean baseline: net worth, monthly income, monthly expenses, and emergency-fund runway\n4. JS - Run Monte Carlo Simulation runs 1,000 randomized month-by-month simulations over the chosen horizon, applying the life event's cash-flow shocks and random market returns each month, tracking ending net worth and whether the balance ever goes negative\n5. AI turns the raw percentiles and shortfall probability into a plain-English narrative, a risk level, and concrete recommendations\n6. The full digital-twin report is emailed to the user - before they make the decision, not after\n\n### How to set up\n1. Import this workflow into n8n\n2. Add Anthropic Claude credentials and attach to the Anthropic model node\n3. Add your account-aggregator API credentials (e.g. Plaid) to Fetch Linked Account Balances & Transactions and replace YOUR_PLAID_CLIENT_ID / YOUR_PLAID_SECRET / YOUR_PLAID_ACCESS_TOKEN\n4. Add your market-data API key to Fetch Historical Market Return Statistics and replace YOUR_MARKET_DATA_API_KEY\n5. Add SMTP credentials to the Send Email node and replace the from/to addresses\n6. Activate the workflow and open the form URL to run a simulation\n\n### Requirements\n- Anthropic Claude API key (claude-sonnet-4 recommended)\n- An account-aggregator API (Plaid, Teller, MX, or similar) for real balances and transactions\n- A market-data API for historical return mean/volatility\n- SMTP credentials for report delivery\n\n### How to customize\n- Adjust ITERATIONS or the mortgage-rate assumption in JS - Run Monte Carlo Simulation\n- Add more life-event types (have a baby, start a business, retire early) as additional cash-flow shock branches\n- Log every simulation run to a Google Sheet or database for a historical view of the user's risk trend over time\n- Add a chart image (via a charting API) embedded in the email showing the P10/P50/P90 net worth fan chart"
},
"typeVersion": 1
},
{
"id": "6de7e4ca-a4da-4c44-a8ec-18dca9a5fba4",
"name": "Sticky Note - Stage 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1872,
64
],
"parameters": {
"color": 3,
"width": 1300,
"height": 760,
"content": "## Stage 1: Real Account Data & Baseline\n\nForm - Request Life Event Simulation captures which event to test and its parameters.\n\nFetch Linked Account Balances & Transactions (API 1) and Fetch Historical Market Return Statistics (API 2) run in parallel off the form submission.\n\nMerge - Combine Account & Market Data joins both API responses into a single item.\n\nJS - Build Financial Baseline & Simulation Inputs (Code Node 1) computes net worth, monthly income/expenses from real transactions, emergency-fund runway, and packages the chosen life event plus market statistics into clean simulation inputs."
},
"typeVersion": 1
},
{
"id": "897d87fa-e9db-44ad-9e0f-9b7d8310eb15",
"name": "Sticky Note - Stage 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
3264,
64
],
"parameters": {
"color": 4,
"width": 900,
"height": 760,
"content": "## Stage 2: Monte Carlo Simulation Engine\n\nJS - Run Monte Carlo Simulation (Code Node 2) runs 1,000 randomized month-by-month paths over the simulation horizon. Each month applies the life event's cash-flow shock (job-loss income drop, or house down payment + new mortgage payment) plus a randomly sampled market return drawn from the real historical mean/volatility.\n\nIt tracks each path's ending net worth and whether the balance ever dipped below zero, then summarizes the results as P10/P50/P90 ending net worth and a shortfall probability.\n\nWait - Simulation Buffer (Wait Node 1) pauses briefly before the AI call to respect API rate limits."
},
"typeVersion": 1
},
{
"id": "dfd1e510-4561-46b2-9471-f3b2c9df87ad",
"name": "Sticky Note - Stage 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
4368,
64
],
"parameters": {
"color": 5,
"width": 892,
"height": 760,
"content": "## Stage 3: AI Narrative & Delivery\n\nThe narrative AI (claude-sonnet-4, temperature 0.3) turns the raw simulation percentiles and shortfall probability into a plain-English summary, a risk level (Low/Moderate/High/Severe), and three concrete recommendations.\n\nJS - Parse AI Output & Format Digital Twin Report (Code Node 3) extracts the structured JSON safely, with a fallback that builds a rule-based summary from the shortfall probability if AI parsing fails, and assembles the full report body.\n\nWait - Rate Limit (Wait Node 2) pauses briefly before delivery, then Send Digital Twin Report Email delivers the full report - baseline, simulated outcomes, risk level, and recommendations - to the user."
},
"typeVersion": 1
},
{
"id": "30fc6478-d072-4f10-94c0-2286900baa52",
"name": "Form - Request Life Event Simulation",
"type": "n8n-nodes-base.formTrigger",
"position": [
1968,
528
],
"parameters": {
"options": {},
"formTitle": "=",
"formFields": {
"values": [
{
"fieldType": "dropdown",
"fieldLabel": "Event Type",
"fieldOptions": {
"values": [
{
"option": "Buy a House"
},
{
"option": "Lose a Job"
},
{
"option": "Both"
}
]
},
"requiredField": true
},
{
"fieldType": "number",
"fieldLabel": "Home Price"
},
{
"fieldType": "number",
"fieldLabel": "Down Payment %"
},
{
"fieldType": "number",
"fieldLabel": "Job Loss Duration Months"
},
{
"fieldType": "number",
"fieldLabel": "Simulation Horizon Months",
"requiredField": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "db7c94ee-8577-4242-89d3-9c53acbc1c11",
"name": "Fetch Linked Account Balances & Transactions",
"type": "n8n-nodes-base.httpRequest",
"position": [
2288,
384
],
"parameters": {
"url": "https://production.plaid.com/accounts/balance/get",
"method": "POST",
"options": {},
"jsonBody": "={\n \"client_id\": \"YOUR_PLAID_CLIENT_ID\",\n \"secret\": \"YOUR_PLAID_SECRET\",\n \"access_token\": \"YOUR_PLAID_ACCESS_TOKEN\"\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "8315c1fc-c808-4384-a889-747f6ed26d1e",
"name": "Fetch Historical Market Return Statistics",
"type": "n8n-nodes-base.httpRequest",
"position": [
2288,
656
],
"parameters": {
"url": "https://api.marketdata-provider.example.com/v1/equities/sp500/return-stats?period=10y",
"options": {},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "484f723f-dcd2-4f1f-9544-a9c767f6c9df",
"name": "Merge - Combine Account & Market Data",
"type": "n8n-nodes-base.merge",
"position": [
2624,
528
],
"parameters": {
"mode": "combine",
"options": {}
},
"typeVersion": 3.2
},
{
"id": "b575984a-9720-420b-8788-3ac4e91cbc79",
"name": "JS - Build Financial Baseline & Simulation Inputs",
"type": "n8n-nodes-base.code",
"position": [
2944,
528
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst formData = $('Form - Request Life Event Simulation').first().json;\n\nconst accounts = item.accounts || [];\nconst transactions = item.recentTransactions || [];\n\nconst totalAssets = accounts\n .filter(a => ['checking', 'savings', 'investment'].includes((a.type || '').toLowerCase()))\n .reduce((sum, a) => sum + (parseFloat(a.balance) || 0), 0);\n\nconst totalLiabilities = accounts\n .filter(a => ['credit', 'loan'].includes((a.type || '').toLowerCase()))\n .reduce((sum, a) => sum + Math.abs(parseFloat(a.balance) || 0), 0);\n\nconst netWorth = Math.round((totalAssets - totalLiabilities) * 100) / 100;\n\n// Recent transactions sampled over roughly 3 months; positive amount = income, negative = expense\nconst monthlyIncome = Math.round(\n (transactions.filter(t => (parseFloat(t.amount) || 0) > 0)\n .reduce((sum, t) => sum + (parseFloat(t.amount) || 0), 0) / 3) * 100\n) / 100;\n\nconst monthlyExpenses = Math.round(\n (Math.abs(transactions.filter(t => (parseFloat(t.amount) || 0) < 0)\n .reduce((sum, t) => sum + (parseFloat(t.amount) || 0), 0)) / 3) * 100\n) / 100;\n\nconst emergencyFundMonths = monthlyExpenses > 0 ? Math.round((totalAssets / monthlyExpenses) * 10) / 10 : null;\n\nconst marketMeanMonthlyReturn = item.meanMonthlyReturn ?? 0.006;\nconst marketStdDevMonthlyReturn = item.stdDevMonthlyReturn ?? 0.04;\n\nconst lifeEvent = {\n type: formData['Event Type'] || 'Custom',\n homePrice: parseFloat(formData['Home Price']) || 0,\n downPaymentPct: parseFloat(formData['Down Payment %']) || 20,\n jobLossDurationMonths: parseInt(formData['Job Loss Duration Months'], 10) || 0\n};\n\nconst horizonMonths = parseInt(formData['Simulation Horizon Months'], 10) || 60;\n\nreturn {\n json: {\n netWorth, totalAssets, totalLiabilities,\n monthlyIncome, monthlyExpenses, emergencyFundMonths,\n marketMeanMonthlyReturn, marketStdDevMonthlyReturn,\n lifeEvent, horizonMonths\n }\n};"
},
"typeVersion": 2
},
{
"id": "662638d6-22e8-4092-a37d-38a6ace6b0ff",
"name": "JS - Run Monte Carlo Simulation",
"type": "n8n-nodes-base.code",
"position": [
3344,
528
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst {\n netWorth, monthlyIncome, monthlyExpenses,\n marketMeanMonthlyReturn, marketStdDevMonthlyReturn,\n lifeEvent, horizonMonths\n} = item;\n\nconst ITERATIONS = 1000;\n\nfunction randomNormal(mean, stdDev) {\n let u = 0, v = 0;\n while (u === 0) u = Math.random();\n while (v === 0) v = Math.random();\n const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);\n return mean + z * stdDev;\n}\n\nconst endingNetWorths = [];\nlet shortfallCount = 0;\n\nfor (let i = 0; i < ITERATIONS; i++) {\n let balance = netWorth;\n let ranOutOfMoney = false;\n\n for (let month = 1; month <= horizonMonths; month++) {\n let income = monthlyIncome;\n let expenses = monthlyExpenses;\n\n if ((lifeEvent.type === 'Lose a Job' || lifeEvent.type === 'Both') && month <= (lifeEvent.jobLossDurationMonths || 0)) {\n income = 0;\n }\n\n if (lifeEvent.type === 'Buy a House' || lifeEvent.type === 'Both') {\n if (month === 1) {\n const downPayment = (lifeEvent.homePrice || 0) * ((lifeEvent.downPaymentPct || 20) / 100);\n balance -= downPayment;\n }\n const loanAmount = (lifeEvent.homePrice || 0) * (1 - (lifeEvent.downPaymentPct || 20) / 100);\n const monthlyMortgage = loanAmount > 0 ? (loanAmount * 0.005) : 0; // approx principal+interest at ~6%/yr\n expenses += monthlyMortgage;\n }\n\n const marketReturn = randomNormal(marketMeanMonthlyReturn, marketStdDevMonthlyReturn);\n balance = balance * (1 + marketReturn) + (income - expenses);\n\n if (balance < 0) ranOutOfMoney = true;\n }\n\n endingNetWorths.push(Math.round(balance * 100) / 100);\n if (ranOutOfMoney) shortfallCount++;\n}\n\nendingNetWorths.sort((a, b) => a - b);\nconst percentile = (p) => endingNetWorths[Math.floor((p / 100) * (endingNetWorths.length - 1))];\n\nconst simulation = {\n iterations: ITERATIONS,\n horizonMonths,\n p10EndingNetWorth: percentile(10),\n p50EndingNetWorth: percentile(50),\n p90EndingNetWorth: percentile(90),\n shortfallProbabilityPct: Math.round((shortfallCount / ITERATIONS) * 1000) / 10\n};\n\nreturn { json: { ...item, simulation } };"
},
"typeVersion": 2
},
{
"id": "69a9b074-2981-4e30-868b-ff8ad170a5f7",
"name": "AI - Generate Risk Narrative & Recommendations",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
3888,
528
],
"parameters": {
"text": "=You are a personal financial planning assistant reviewing a Monte Carlo stress test run against a user's real linked accounts.\n\nCurrent Baseline:\nNet Worth: ${{ $json.netWorth }}\nMonthly Income: ${{ $json.monthlyIncome }}\nMonthly Expenses: ${{ $json.monthlyExpenses }}\nEmergency Fund Coverage: {{ $json.emergencyFundMonths }} months\n\nLife Event Tested: {{ $json.lifeEvent.type }}\nHome Price: {{ $json.lifeEvent.homePrice }} | Down Payment: {{ $json.lifeEvent.downPaymentPct }}% | Job Loss Duration: {{ $json.lifeEvent.jobLossDurationMonths }} months\nSimulation Horizon: {{ $json.horizonMonths }} months across {{ $json.simulation.iterations }} Monte Carlo runs\n\nSimulated Outcomes:\nP10 (pessimistic) ending net worth: ${{ $json.simulation.p10EndingNetWorth }}\nP50 (median) ending net worth: ${{ $json.simulation.p50EndingNetWorth }}\nP90 (optimistic) ending net worth: ${{ $json.simulation.p90EndingNetWorth }}\nProbability of running out of money at some point: {{ $json.simulation.shortfallProbabilityPct }}%\n\nWrite a plain-English narrative a non-expert could understand, assign one risk level, and give three short, concrete, actionable recommendations.\n\nReturn a JSON object with EXACTLY these fields:\n{\n \"narrativeSummary\": \"3-4 sentences, plain English, specific to these numbers\",\n \"riskLevel\": \"One of: Low, Moderate, High, Severe\",\n \"topRecommendations\": [\"short actionable recommendation 1\", \"short actionable recommendation 2\", \"short actionable recommendation 3\"]\n}\n\nReturn ONLY the JSON. No markdown, no explanation.",
"options": {},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "27ae6f59-2fb6-474f-83f8-06628d7dcc87",
"name": "Anthropic - Narrative Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
3760,
672
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-20250514"
},
"options": {
"temperature": 0.3
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "ec635561-b330-4200-9bee-790032cf72b2",
"name": "JS - Parse AI Output & Format Digital Twin Report",
"type": "n8n-nodes-base.code",
"position": [
4448,
528
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n const rawText = item.output || item.text || item.response || '{}';\n const clean = rawText.replace(/```json|```/g, '').trim();\n parsed = JSON.parse(clean);\n} catch (e) {\n const pct = item.simulation.shortfallProbabilityPct;\n const risk = pct >= 25 ? 'Severe' : pct >= 10 ? 'High' : pct >= 3 ? 'Moderate' : 'Low';\n parsed = {\n narrativeSummary: `Simulating ${item.lifeEvent.type} over ${item.horizonMonths} months against your real accounts shows a ${pct}% chance your balance goes negative at some point. Median ending net worth is $${item.simulation.p50EndingNetWorth}.`,\n riskLevel: risk,\n topRecommendations: [\n 'Build a larger cash buffer before taking on this life event.',\n 'Reassess monthly expenses to create more breathing room.',\n 'Re-run this simulation after adjusting the event parameters.'\n ]\n };\n}\n\nconst reportGeneratedAt = new Date().toISOString();\nconst recommendationsText = (parsed.topRecommendations || [])\n .map((r, i) => `${i + 1}. ${r}`)\n .join('\\n');\n\nconst reportBody = `Personal Financial Digital Twin Report\\nGenerated: ${reportGeneratedAt}\\n\\nLife Event Tested: ${item.lifeEvent.type}\\nSimulation Horizon: ${item.horizonMonths} months (${item.simulation.iterations} Monte Carlo runs)\\n\\nCurrent Net Worth: $${item.netWorth}\\nMonthly Income: $${item.monthlyIncome} | Monthly Expenses: $${item.monthlyExpenses}\\nEmergency Fund Coverage: ${item.emergencyFundMonths} months\\n\\nSimulated Outcomes:\\n- Pessimistic (P10) Ending Net Worth: $${item.simulation.p10EndingNetWorth}\\n- Median (P50) Ending Net Worth: $${item.simulation.p50EndingNetWorth}\\n- Optimistic (P90) Ending Net Worth: $${item.simulation.p90EndingNetWorth}\\n- Probability of Running Out of Money: ${item.simulation.shortfallProbabilityPct}%\\n\\nRisk Level: ${parsed.riskLevel}\\n\\n${parsed.narrativeSummary}\\n\\nRecommendations:\\n${recommendationsText}`;\n\nreturn {\n json: {\n ...item,\n ...parsed,\n reportGeneratedAt,\n reportBody\n }\n};"
},
"typeVersion": 2
},
{
"id": "cf520f3c-0eb8-4cc2-9a43-c2039e5d3e0c",
"name": "Send Digital Twin Report Email",
"type": "n8n-nodes-base.emailSend",
"position": [
5056,
528
],
"parameters": {
"options": {},
"subject": "=Your Financial Digital Twin Report - Risk Level: {{ $json.riskLevel }}",
"toEmail": "user@example.com",
"fromEmail": "user@example.com"
},
"credentials": {
"smtp": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "87264ae1-1b29-4c52-b56f-cca2f1f8db66",
"name": "Wait For Rate Limit",
"type": "n8n-nodes-base.wait",
"position": [
4768,
528
],
"parameters": {},
"typeVersion": 1.1
},
{
"id": "1d192753-871d-4c1a-8d3c-345542157c98",
"name": "Wait For Simulation Buffer",
"type": "n8n-nodes-base.wait",
"position": [
3600,
528
],
"parameters": {},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "dbd5b762-9342-44c6-ad96-2ca7ea441bb7",
"connections": {
"Wait For Rate Limit": {
"main": [
[
{
"node": "Send Digital Twin Report Email",
"type": "main",
"index": 0
}
]
]
},
"Wait For Simulation Buffer": {
"main": [
[
{
"node": "AI - Generate Risk Narrative & Recommendations",
"type": "main",
"index": 0
}
]
]
},
"Anthropic - Narrative Model": {
"ai_languageModel": [
[
{
"node": "AI - Generate Risk Narrative & Recommendations",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"JS - Run Monte Carlo Simulation": {
"main": [
[
{
"node": "Wait For Simulation Buffer",
"type": "main",
"index": 0
}
]
]
},
"Form - Request Life Event Simulation": {
"main": [
[
{
"node": "Fetch Linked Account Balances & Transactions",
"type": "main",
"index": 0
},
{
"node": "Fetch Historical Market Return Statistics",
"type": "main",
"index": 0
}
]
]
},
"Merge - Combine Account & Market Data": {
"main": [
[
{
"node": "JS - Build Financial Baseline & Simulation Inputs",
"type": "main",
"index": 0
}
]
]
},
"Fetch Historical Market Return Statistics": {
"main": [
[
{
"node": "Merge - Combine Account & Market Data",
"type": "main",
"index": 1
}
]
]
},
"Fetch Linked Account Balances & Transactions": {
"main": [
[
{
"node": "Merge - Combine Account & Market Data",
"type": "main",
"index": 0
}
]
]
},
"AI - Generate Risk Narrative & Recommendations": {
"main": [
[
{
"node": "JS - Parse AI Output & Format Digital Twin Report",
"type": "main",
"index": 0
}
]
]
},
"JS - Build Financial Baseline & Simulation Inputs": {
"main": [
[
{
"node": "JS - Run Monte Carlo Simulation",
"type": "main",
"index": 0
}
]
]
},
"JS - Parse AI Output & Format Digital Twin Report": {
"main": [
[
{
"node": "Wait For Rate Limit",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
anthropicApismtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow collects a user-selected financial life event through an n8n Form, retrieves account and market data, runs a Monte Carlo stress test, and uses Anthropic Claude to generate and email a personalized financial report. Receives a form submission that specifies the life…
Source: https://n8n.io/workflows/17361/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
How it Works
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Template Nodes Example. Uses CUSTOM, formTrigger, executeWorkflowTrigger, chatTrigger. Event-driven trigger; 70 nodes.
Generate research-backed article with n8n
This workflow is designed for marketers, founders, agencies, and product teams who want to understand how real customers talk about a product category, market, or problem space.