This workflow corresponds to n8n.io template #16444 — we link there as the canonical source.
This workflow follows the Gmail → HTTP Request 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": "nDUmmSSyE44AToxe",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI Deal Risk Monitor",
"tags": [],
"nodes": [
{
"id": "16bff77c-67c4-4594-b10e-f79c79539b86",
"name": "Clean & Prepare Deal Data",
"type": "n8n-nodes-base.code",
"notes": "Normalizes all opportunity fields, computes derived metrics (daysSinceLastActivity, daysUntilClose, daysInPipeline), generates workflowRunId, and flags missing fields.",
"position": [
6688,
3056
],
"parameters": {
"jsCode": "const item = $input.item.json;\n\nconst missingFields = [];\nif (!item.Id) missingFields.push('Id');\nif (!item.CloseDate) missingFields.push('CloseDate');\nif (!item.StageName) missingFields.push('StageName');\nif (!item.Owner?.Email) missingFields.push('Owner.Email');\n\nconst ownerEmail = item.Owner?.Email ?? '';\nconst closeDate = item.CloseDate ?? null;\nconst lastActivityDate = item.LastActivityDate ?? null;\nconst now = Date.now();\n\nconst daysSinceLastActivity = lastActivityDate\n ? Math.floor((now - new Date(lastActivityDate).getTime()) / 86400000)\n : 999;\n\nconst daysUntilClose = closeDate\n ? Math.floor((new Date(closeDate).getTime() - now) / 86400000)\n : -1;\n\nconst createdDate = item.CreatedDate ?? null;\nconst daysInPipeline = createdDate\n ? Math.floor((now - new Date(createdDate).getTime()) / 86400000)\n : 0;\n\nconst workflowRunId = `WF-${$execution.id ?? 'manual'}-OPP-${item.Id ?? 'unknown'}-${Date.now()}`;\n\nreturn [{\n json: {\n workflowRunId,\n opportunityId: item.Id ?? '',\n opportunityName: item.Name ?? 'Unknown Opportunity',\n accountId: item.AccountId ?? '',\n accountName: item.Account?.Name ?? 'Unknown Account',\n ownerName: item.Owner?.Name ?? 'Unknown Owner',\n ownerEmail,\n stageName: item.StageName ?? 'Unknown',\n dealAmount: item.Amount ?? 0,\n closeDate: closeDate ?? 'No Close Date',\n probability: item.Probability ?? 0,\n lastActivityDate: lastActivityDate ?? 'No Activity Recorded',\n lastModifiedDate: item.LastModifiedDate ?? '',\n createdDate: createdDate ?? '',\n forecastCategory: item.ForecastCategory ?? 'Omitted',\n dealDescription: (item.Description ?? 'No description available').substring(0, 500),\n daysSinceLastActivity,\n daysUntilClose,\n daysInPipeline,\n missingFields,\n hasOwnerEmail: ownerEmail.length > 0,\n workflowRunDate: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "27289363-3753-41bb-a062-0c85ce1bc2b7",
"name": "Standardize Deal Fields (Set Node)",
"type": "n8n-nodes-base.set",
"notes": "NEW: Set node ensures all downstream nodes receive consistently typed and named fields. Prevents undefined/null bleed-through. Acts as a single source of truth for field contracts.",
"position": [
6912,
3056
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "set_workflowRunId",
"name": "workflowRunId",
"type": "string",
"value": "={{ $json.workflowRunId }}"
},
{
"id": "set_opportunityId",
"name": "opportunityId",
"type": "string",
"value": "={{ $json.opportunityId }}"
},
{
"id": "set_opportunityName",
"name": "opportunityName",
"type": "string",
"value": "={{ $json.opportunityName }}"
},
{
"id": "set_accountName",
"name": "accountName",
"type": "string",
"value": "={{ $json.accountName }}"
},
{
"id": "set_ownerName",
"name": "ownerName",
"type": "string",
"value": "={{ $json.ownerName }}"
},
{
"id": "set_ownerEmail",
"name": "ownerEmail",
"type": "string",
"value": "={{ $json.ownerEmail }}"
},
{
"id": "set_stageName",
"name": "stageName",
"type": "string",
"value": "={{ $json.stageName }}"
},
{
"id": "set_dealAmount",
"name": "dealAmount",
"type": "number",
"value": "={{ $json.dealAmount ?? 0 }}"
},
{
"id": "set_closeDate",
"name": "closeDate",
"type": "string",
"value": "={{ $json.closeDate }}"
},
{
"id": "set_probability",
"name": "probability",
"type": "number",
"value": "={{ $json.probability ?? 0 }}"
},
{
"id": "set_forecastCategory",
"name": "forecastCategory",
"type": "string",
"value": "={{ $json.forecastCategory }}"
},
{
"id": "set_daysSinceLastActivity",
"name": "daysSinceLastActivity",
"type": "number",
"value": "={{ $json.daysSinceLastActivity ?? 999 }}"
},
{
"id": "set_daysUntilClose",
"name": "daysUntilClose",
"type": "number",
"value": "={{ $json.daysUntilClose ?? -1 }}"
},
{
"id": "set_daysInPipeline",
"name": "daysInPipeline",
"type": "number",
"value": "={{ $json.daysInPipeline ?? 0 }}"
},
{
"id": "set_dealDescription",
"name": "dealDescription",
"type": "string",
"value": "={{ $json.dealDescription }}"
},
{
"id": "set_missingFields",
"name": "missingFields",
"type": "array",
"value": "={{ $json.missingFields }}"
},
{
"id": "set_hasOwnerEmail",
"name": "hasOwnerEmail",
"type": "boolean",
"value": "={{ $json.hasOwnerEmail }}"
},
{
"id": "set_workflowRunDate",
"name": "workflowRunDate",
"type": "string",
"value": "={{ $json.workflowRunDate }}"
},
{
"id": "set_lastActivityDate",
"name": "lastActivityDate",
"type": "string",
"value": "={{ $json.lastActivityDate }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "16cc139d-fdd1-4677-8233-753e6b46e020",
"name": "Check for Missing Deal Information",
"type": "n8n-nodes-base.if",
"notes": "TRUE = missing critical fields \u2192 manual review branch. FALSE = proceed to pre-AI triage.",
"position": [
7136,
3056
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "guard_missing_fields",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.missingFields.length }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2
},
{
"id": "b970a417-4f22-4e25-be34-b529b00fa287",
"name": "Send Incomplete Deals for Manual Review",
"type": "n8n-nodes-base.code",
"notes": "Catches opportunities missing CloseDate, StageName, Id, or Owner.Email. Routes to Slack incomplete deals alert.",
"position": [
7360,
3248
],
"parameters": {
"jsCode": "const item = $input.item.json;\nconsole.warn(`[MANUAL REVIEW] Opportunity ${item.opportunityId} is missing fields: ${item.missingFields.join(', ')}`);\nreturn [{ json: { ...item, routedTo: 'manual_review', reason: `Missing required fields: ${item.missingFields.join(', ')}` } }];"
},
"typeVersion": 2
},
{
"id": "2a802c31-5084-4639-bdfe-6048a0575e42",
"name": "Alert Slack on Incomplete Deal Data",
"type": "n8n-nodes-base.slack",
"notes": "Posts incomplete deal records to Slack so a human can fix the CRM data.",
"position": [
7584,
3248
],
"parameters": {
"text": "={{ ' Deal skipped due to missing data: ' + $json.opportunityName + ' | Missing: ' + $json.missingFields.join(', ') }}",
"select": "channel",
"blocksUi": {
"blocksValues": [
{
"type": "header",
"textUi": {
"text": " Deal Skipped \u2014 Missing CRM Data",
"emoji": true
}
},
{
"type": "section",
"textUi": {
"text": "={{ '*Opportunity ID:* ' + $json.opportunityId + '\\n*Opportunity Name:* ' + $json.opportunityName + '\\n*Missing Fields:* ' + $json.missingFields.join(', ') + '\\n*Reason:* ' + $json.reason + '\\n*Run ID:* ' + $json.workflowRunId }}"
}
}
]
},
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0ATD2VAH3K",
"cachedResultName": "all-akn8ndemo"
},
"messageType": "block",
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3,
"continueOnFail": true
},
{
"id": "bbb735cf-9fa6-43f9-abb7-bf2258583d9b",
"name": "Filter Important Deals (Risky / High Value)",
"type": "n8n-nodes-base.if",
"notes": "Heuristic gate: pass deals to AI only if inactive 21+ days, closing within 30 days, probability <60%, OR high-value ($50k+). Saves AI token cost.",
"position": [
7360,
2832
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "triage_inactive",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.daysSinceLastActivity }}",
"rightValue": 21
},
{
"id": "triage_close_pressure",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $json.daysUntilClose }}",
"rightValue": 30
},
{
"id": "triage_low_probability",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $json.probability }}",
"rightValue": 60
},
{
"id": "triage_high_value",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.dealAmount }}",
"rightValue": 50000
}
]
}
},
"typeVersion": 2
},
{
"id": "0e8bce7e-26a9-4138-a322-20a9b0a7e6d8",
"name": "Mark Deal as Healthy (Skip AI Analysis)",
"type": "n8n-nodes-base.salesforce",
"notes": "IMPROVED: Now also writes workflowRunId for traceability on healthy deals. Saves token cost by skipping AI.",
"position": [
7584,
3056
],
"parameters": {
"resource": "opportunity",
"operation": "update",
"updateFields": {
"customFieldsUi": {
"customFieldsValues": [
{
"value": "=0",
"fieldId": "Risk_Score__c"
},
{
"value": "=Low",
"fieldId": "Risk_Level__c"
},
{
"value": "=Deal passed heuristic triage as healthy. No AI analysis required at this time.",
"fieldId": "Risk_Summary__c"
},
{
"value": "=Healthy",
"fieldId": "Deal_Health__c"
},
{
"value": "={{ $json.workflowRunDate }}",
"fieldId": "Risk_Last_Assessed__c"
},
{
"value": "={{ $json.workflowRunId }}",
"fieldId": "Workflow_Run_ID__c"
}
]
}
},
"opportunityId": "={{ $json.opportunityId }}"
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "ef6c1f28-2217-4150-b9a9-e6f08c8d2aad",
"name": "Get Deal Emails from Gmail",
"type": "n8n-nodes-base.gmail",
"notes": "Searches Gmail using owner email + opportunity/account name. continueOnFail prevents halt on auth failure.",
"position": [
7584,
2848
],
"parameters": {
"limit": 20,
"filters": {
"q": "={{ ($json.hasOwnerEmail ? ('from:' + $json.ownerEmail + ' OR to:' + $json.ownerEmail) : '') + ' ' + $json.opportunityName + ' ' + $json.accountName }}"
},
"operation": "getAll"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1,
"continueOnFail": true
},
{
"id": "b539c180-8357-41c0-bcc8-95fbfd311680",
"name": "Get Deal Emails from Outlook",
"type": "n8n-nodes-base.microsoftOutlook",
"notes": "Fetches Outlook emails. continueOnFail prevents workflow halt if Outlook creds fail.",
"position": [
7584,
2640
],
"parameters": {
"limit": 20,
"options": {},
"operation": "getAll"
},
"typeVersion": 2,
"continueOnFail": true
},
{
"id": "1ac7ecba-e2b5-4061-a4a5-c28cd0614605",
"name": "Combine All Email Data",
"type": "n8n-nodes-base.code",
"notes": "Merges Gmail and Outlook email items safely. Emits sentinel item if both return zero emails so AI still runs on CRM data alone.",
"position": [
7808,
2768
],
"parameters": {
"jsCode": "let allItems = [];\n\ntry {\n const gmailItems = $('Get Deal Emails from Gmail').all();\n if (Array.isArray(gmailItems)) allItems.push(...gmailItems);\n} catch(e) {\n console.warn('[EMAIL MERGE] Gmail read failed or returned no data:', e.message);\n}\n\ntry {\n const outlookItems = $('Get Deal Emails from Outlook').all();\n if (Array.isArray(outlookItems)) allItems.push(...outlookItems);\n} catch(e) {\n console.warn('[EMAIL MERGE] Outlook read failed or returned no data:', e.message);\n}\n\n// Filter out error sentinel items\nallItems = allItems.filter(item => item.json && !item.json.source?.includes('_error') && !item.json.error);\n\nif (allItems.length === 0) {\n console.warn('[EMAIL MERGE] No emails found from Gmail or Outlook. Passing empty context to AI.');\n return [{ json: { _noEmailData: true } }];\n}\n\nreturn allItems;"
},
"typeVersion": 2
},
{
"id": "e2c720ef-83e5-4a52-8473-6ef8dc6325e7",
"name": "Prepare Email Insights for AI",
"type": "n8n-nodes-base.code",
"notes": "IMPROVED: Fixed internal email detection to use owner domain (not full email). Deduplicates by ID and subject+date. Caps snippets to 300 chars. Prioritizes customer-facing emails.",
"position": [
8096,
2768
],
"parameters": {
"jsCode": "const opportunityData = $('Standardize Deal Fields (Set Node)').item.json;\nconst emailItems = $input.all().filter(i => !i.json._noEmailData);\nconst ownerEmail = opportunityData.ownerEmail?.toLowerCase() ?? '';\nconst ownerDomain = ownerEmail.includes('@') ? ownerEmail.split('@')[1] : '__no_domain__';\n\nconst excludePatterns = [\n /no.?reply/i, /noreply/i, /donotreply/i, /notifications?@/i,\n /alerts?@/i, /support@/i, /hello@/i, /info@/i, /admin@/i,\n /calendar/i, /automated/i, /bounce/i, /mailer-daemon/i\n];\n\nconst seenIds = new Set();\nconst seenSubjectDates = new Set();\nlet emailSummaries = [];\nlet externalSenders = new Set();\nlet customerRepliesFound = 0;\nlet latestEmailDate = null;\nlet internalEmailCount = 0;\nlet externalEmailCount = 0;\n\nfor (const item of emailItems) {\n const email = item.json;\n if (!email || email.source?.includes('_error') || email.error) continue;\n\n const msgId = email.id || email.messageId || email.internetMessageId || null;\n const subject = (email.subject || email.Subject || 'No Subject').trim();\n const receivedRaw = email.internalDate\n ? new Date(parseInt(email.internalDate)).toISOString()\n : (email.receivedDateTime || email.date || 'Unknown Date');\n const subjectDateKey = `${subject}::${receivedRaw.substring(0, 10)}`;\n\n if (msgId && seenIds.has(msgId)) continue;\n if (seenSubjectDates.has(subjectDateKey)) continue;\n if (msgId) seenIds.add(msgId);\n seenSubjectDates.add(subjectDateKey);\n\n const senderRaw = email.from?.emailAddress?.address || email.from?.value?.[0]?.address || email.from || 'unknown@unknown.com';\n const senderEmail = senderRaw.toString().toLowerCase();\n const senderName = email.from?.emailAddress?.name || senderEmail;\n\n if (excludePatterns.some(p => p.test(senderEmail) || p.test(subject))) continue;\n\n // FIX: Use ownerDomain instead of full ownerEmail for internal detection\n const isInternal = ownerDomain !== '__no_domain__' && senderEmail.endsWith('@' + ownerDomain);\n if (isInternal) internalEmailCount++; else externalEmailCount++;\n\n if (!isInternal && senderEmail !== ownerEmail) {\n externalSenders.add(senderEmail);\n customerRepliesFound++;\n }\n\n const rawSnippet = email.snippet || email.bodyPreview || email.body?.content || '';\n const snippet = rawSnippet\n .replace(/<[^>]+>/g, '')\n .replace(/\\s+/g, ' ')\n .replace(/--+.*$/gm, '')\n .trim()\n .substring(0, 300);\n\n if (latestEmailDate === null || new Date(receivedRaw) > new Date(latestEmailDate)) {\n latestEmailDate = receivedRaw;\n }\n\n emailSummaries.push({\n receivedDate: receivedRaw,\n sender: senderName,\n senderEmail,\n subject: subject.substring(0, 120),\n snippet,\n isExternal: !isInternal\n });\n}\n\nemailSummaries.sort((a, b) => new Date(b.receivedDate) - new Date(a.receivedDate));\n\nconst externalEmails = emailSummaries.filter(e => e.isExternal);\nconst internalEmails = emailSummaries.filter(e => !e.isExternal);\nconst prioritized = [...externalEmails.slice(0, 8), ...internalEmails.slice(0, 4)];\n\nconst emailContextLines = prioritized.map(e =>\n `[${e.receivedDate.substring(0,10)}] From: ${e.sender} <${e.senderEmail}> | Subject: ${e.subject} | Preview: ${e.snippet}`\n);\n\nconst emailContext = emailContextLines.length > 0\n ? emailContextLines.join('\\n')\n : 'No recent deal-related email activity found.';\n\nconst silenceGapDays = latestEmailDate\n ? Math.floor((Date.now() - new Date(latestEmailDate).getTime()) / 86400000)\n : opportunityData.daysSinceLastActivity;\n\nreturn [{\n json: {\n ...opportunityData,\n emailContext,\n totalEmailsAnalyzed: emailSummaries.length,\n externalEmailCount,\n internalEmailCount,\n uniqueExternalParticipants: externalSenders.size,\n customerRepliesFound,\n silenceGapDays,\n hasCustomerReply: customerRepliesFound > 0,\n internalToExternalRatio: externalEmailCount > 0\n ? (internalEmailCount / externalEmailCount).toFixed(2)\n : 'N/A'\n }\n}];"
},
"typeVersion": 2
},
{
"id": "f8ae5974-bee9-4229-bfe5-f92bcd2af21d",
"name": "Analyze Deal Risk Using AI",
"type": "@n8n/n8n-nodes-langchain.openAi",
"notes": "IMPROVED: Temperature set to 0.2 for deterministic, consistent scoring. continueOnFail routes AI failures gracefully.",
"position": [
8288,
2768
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {
"temperature": 0.2
},
"responses": {
"values": [
{
"role": "system",
"content": "You are a senior sales intelligence analyst specializing in B2B deal risk assessment. Your job is to analyze CRM data and email communication patterns to identify at-risk sales opportunities before they are lost. You always respond in strict JSON format with no additional commentary, no markdown, and no code fences outside the JSON object."
},
{
"content": "=You are a B2B sales risk analyst.\n\nYour task is to analyze the sales opportunity below and generate a structured deal risk assessment based on CRM data and recent email activity.\n\n## OPPORTUNITY DETAILS\n- Opportunity Name: {{ $json.opportunityName }}\n- Account Name: {{ $json.accountName }}\n- Owner Name: {{ $json.ownerName }}\n- Stage Name: {{ $json.stageName }}\n- Deal Amount: ${{ $json.dealAmount }}\n- Close Date: {{ $json.closeDate }}\n- Days Until Close: {{ $json.daysUntilClose }}\n- Probability: {{ $json.probability }}%\n- Forecast Category: {{ $json.forecastCategory }}\n- Days Since Last Activity: {{ $json.daysSinceLastActivity }}\n- Days in Pipeline: {{ $json.daysInPipeline }}\n- Last Activity Date: {{ $json.lastActivityDate }}\n- Deal Description: {{ $json.dealDescription }}\n\n## EMAIL COMMUNICATION SIGNALS\n- Total Emails Found: {{ $json.totalEmailsAnalyzed }}\n- External (Customer-Side) Emails: {{ $json.externalEmailCount }}\n- Internal Emails: {{ $json.internalEmailCount }}\n- Unique External Participants: {{ $json.uniqueExternalParticipants }}\n- Customer Replies Detected: {{ $json.customerRepliesFound }}\n- Days Since Last Email Activity (Silence Gap): {{ $json.silenceGapDays }}\n- Internal-to-External Ratio: {{ $json.internalToExternalRatio }}\n\n### Email Context (Most Recent First)\n{{ $json.emailContext }}\n\n## RISK ANALYSIS OBJECTIVE\nEvaluate this opportunity for deal risk using the following signals:\n1. Inactivity - long gap since last engagement or CRM activity\n2. Stage Stagnation - stuck in stage, approaching close without progression\n3. Email Sentiment - negative, hesitant, evasive, or delayed communication\n4. Response Delays - long lags between exchanges\n5. Stakeholder Disengagement - fewer contacts, reduced frequency\n6. Close Date Pressure - near close date with low confidence\n7. Forecast Misalignment - forecast too optimistic vs engagement signals\n8. Buyer Silence - no customer-side replies detected recently\n9. Internal-Only Activity - high ratio of internal emails suggests no external engagement\n\n## SCORING GUIDELINES\n- 0-20: Very low risk\n- 21-40: Manageable risk\n- 41-60: Moderate concern\n- 61-80: High likelihood of slippage or loss\n- 81-100: Critical, likely lost without immediate intervention\n\n## OUTPUT RULES\n- Return ONLY valid JSON\n- Do NOT include markdown, explanation, or extra text\n- Do NOT wrap in code blocks\n- All fields must always be present\n- Arrays must be present even if empty: []\n- riskScore must be an integer 0-100\n- riskLevel must be exactly one of: Low, Medium, High, Critical\n- estimatedDealHealth must be exactly one of: Healthy, Needs Attention, At Risk, Critical\n\n## REQUIRED OUTPUT FORMAT\n{\n \"riskScore\": <integer 0-100>,\n \"riskLevel\": \"<Low|Medium|High|Critical>\",\n \"riskSummary\": \"<2-3 sentence executive summary>\",\n \"riskFactors\": [\"<risk signal 1>\", \"<risk signal 2>\", \"<risk signal 3>\"],\n \"positiveSignals\": [\"<positive indicator 1>\"],\n \"recommendedActions\": [\"<action 1>\", \"<action 2>\", \"<action 3>\"],\n \"urgencyFlag\": <true|false>,\n \"estimatedDealHealth\": \"<Healthy|Needs Attention|At Risk|Critical>\",\n \"topRiskDriver\": \"<single most critical risk factor>\",\n \"confidenceScore\": <integer 0-100 indicating AI confidence in this assessment>\n}"
}
]
},
"builtInTools": {}
},
"typeVersion": 2.1,
"continueOnFail": true
},
{
"id": "7799b72a-c5bd-4a98-bfcc-dd940c2157b9",
"name": "Validate & Clean AI Results",
"type": "n8n-nodes-base.code",
"notes": "IMPROVED: Added console.error for parse failures with raw preview. Uses SALESFORCE_INSTANCE_URL env var for opportunity link.",
"position": [
8608,
2768
],
"parameters": {
"jsCode": "const inputData = $('Prepare Email Insights for AI').item.json;\nconst aiResponseRaw = $input.item.json.message?.content\n || $input.item.json.choices?.[0]?.message?.content\n || '{}';\n\nconst VALID_RISK_LEVELS = ['Low', 'Medium', 'High', 'Critical'];\nconst VALID_DEAL_HEALTH = ['Healthy', 'Needs Attention', 'At Risk', 'Critical'];\n\nlet aiResult;\ntry {\n const cleaned = aiResponseRaw\n .replace(/```json\\n?/gi, '')\n .replace(/```\\n?/gi, '')\n .trim();\n aiResult = JSON.parse(cleaned);\n if (typeof aiResult !== 'object' || aiResult === null || aiResult.riskScore === undefined) {\n throw new Error('Invalid AI JSON structure \u2014 missing riskScore');\n }\n} catch (e) {\n console.error('[AI PARSE ERROR]', e.message, '| Raw:', aiResponseRaw.substring(0, 200));\n aiResult = {\n riskScore: 50,\n riskLevel: 'Medium',\n riskSummary: 'AI analysis could not be parsed. Manual review recommended.',\n riskFactors: ['AI response parsing error \u2014 manual review required'],\n positiveSignals: [],\n recommendedActions: ['Review opportunity manually', 'Check email communications directly', 'Update opportunity in CRM'],\n urgencyFlag: false,\n estimatedDealHealth: 'Needs Attention',\n topRiskDriver: 'AI parsing failure',\n confidenceScore: 0\n };\n}\n\nlet riskScore = Math.min(100, Math.max(0, Math.round(Number(aiResult.riskScore ?? 50))));\nif (isNaN(riskScore)) riskScore = 50;\n\nconst riskLevel = VALID_RISK_LEVELS.includes(aiResult.riskLevel) ? aiResult.riskLevel : 'Medium';\nconst estimatedDealHealth = VALID_DEAL_HEALTH.includes(aiResult.estimatedDealHealth) ? aiResult.estimatedDealHealth : 'Needs Attention';\n\nconst riskFactorsArr = Array.isArray(aiResult.riskFactors) ? aiResult.riskFactors : [];\nconst positiveSignalsArr = Array.isArray(aiResult.positiveSignals) ? aiResult.positiveSignals : [];\nconst recommendedActionsArr = Array.isArray(aiResult.recommendedActions) ? aiResult.recommendedActions : [];\n\nconst riskSummary = (aiResult.riskSummary ?? '').substring(0, 1000);\nconst riskFactorsText = riskFactorsArr.join('; ').substring(0, 2000);\nconst recommendedActionsText = recommendedActionsArr.join('; ').substring(0, 2000);\nconst topRiskDriver = (aiResult.topRiskDriver ?? '').substring(0, 255);\nconst confidenceScore = Math.min(100, Math.max(0, Math.round(Number(aiResult.confidenceScore ?? 50))));\n\n// Alert cooldown: suppress duplicate alerts for 24h unless risk worsened\nconst previousRiskLevel = inputData.previousRiskLevel ?? null;\nconst alertCooldownHours = 24;\nconst lastAlertTime = inputData.lastAlertSentAt ? new Date(inputData.lastAlertSentAt) : null;\nconst cooldownExpired = !lastAlertTime || (Date.now() - lastAlertTime.getTime()) > alertCooldownHours * 3600000;\nconst riskWorsened = previousRiskLevel && VALID_RISK_LEVELS.indexOf(riskLevel) > VALID_RISK_LEVELS.indexOf(previousRiskLevel);\nconst shouldSendAlert = cooldownExpired || riskWorsened;\n\n// Option 1: Use process.env instead of $env\n// const sfInstanceUrl = process?.env?.SALESFORCE_INSTANCE_URL || 'https://yourinstance.salesforce.com';\n\n// Option 2: Pass Salesforce URL via input data (RECOMMENDED for n8n)\nconst sfInstanceUrl = inputData.salesforceInstanceUrl || 'https://yourinstance.salesforce.com';\n\nconst opportunityLink = `${sfInstanceUrl}/${inputData.opportunityId}`;\n\nreturn [{\n json: {\n ...inputData,\n riskScore,\n riskLevel,\n riskSummary,\n riskFactors: riskFactorsText,\n positiveSignals: positiveSignalsArr.join('; ').substring(0, 1000),\n recommendedActions: recommendedActionsText,\n urgencyFlag: aiResult.urgencyFlag === true,\n estimatedDealHealth,\n topRiskDriver,\n confidenceScore,\n rawRiskFactorsArray: riskFactorsArr,\n rawRecommendedActionsArray: recommendedActionsArr,\n shouldSendAlert,\n opportunityLink\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4c6e2989-f2a8-4c4a-8835-99e34542abc8",
"name": "Is This Deal Risky Enough to Act?",
"type": "n8n-nodes-base.if",
"notes": "TRUE = Medium/High/Critical \u2192 alert + update path. FALSE = Low risk \u2192 log only.",
"position": [
8832,
2768
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition_001",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.riskLevel }}",
"rightValue": "Low"
}
]
}
},
"typeVersion": 2
},
{
"id": "4690da50-bb0d-4836-87d3-3517f6eb56b1",
"name": "Update Deal Risk in Salesforce",
"type": "n8n-nodes-base.salesforce",
"notes": "Writes validated risk fields to Salesforce. Includes workflowRunId, topRiskDriver, and confidenceScore for full traceability.",
"position": [
9072,
2608
],
"parameters": {
"resource": "opportunity",
"operation": "update",
"updateFields": {
"customFieldsUi": {
"customFieldsValues": [
{
"value": "={{ $json.riskScore }}",
"fieldId": "Risk_Score__c"
},
{
"value": "={{ $json.riskLevel }}",
"fieldId": "Risk_Level__c"
},
{
"value": "={{ $json.riskSummary }}",
"fieldId": "Risk_Summary__c"
},
{
"value": "={{ $json.riskFactors }}",
"fieldId": "=Risk_Factor__c"
},
{
"value": "={{ $json.recommendedActions }}",
"fieldId": "=Recomended_Actions__c"
},
{
"value": "={{ $json.estimatedDealHealth }}",
"fieldId": "Deal_Health__c"
},
{
"value": "={{ $json.workflowRunDate }}",
"fieldId": "Risk_Last_Assessed__c"
},
{
"value": "={{ $json.workflowRunId }}",
"fieldId": "=Worklfow_Run_ID__c"
},
{
"value": "={{ $json.topRiskDriver }}",
"fieldId": "Top_Risk_Driver__c"
},
{
"value": "={{ $json.confidenceScore }}",
"fieldId": "Risk_Confidence_Score__c"
}
]
}
},
"opportunityId": "={{ $json.opportunityId }}"
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "05b3b66f-63ee-4e46-8ce6-8f9152f7de3a",
"name": "Save Low-Risk Deal Status",
"type": "n8n-nodes-base.salesforce",
"notes": "Logs low-risk deal assessments to Salesforce with minimal field updates including workflowRunId.",
"position": [
9072,
3008
],
"parameters": {
"resource": "opportunity",
"operation": "update",
"updateFields": {
"customFieldsUi": {
"customFieldsValues": [
{
"value": "={{ $json.riskScore }}",
"fieldId": "Risk_Score__c"
},
{
"value": "={{ $json.riskLevel }}",
"fieldId": "Risk_Level__c"
},
{
"value": "={{ $json.workflowRunDate }}",
"fieldId": "Risk_Last_Assessed__c"
},
{
"value": "={{ $json.workflowRunId }}",
"fieldId": "Workflow_Run_ID__c"
}
]
}
},
"opportunityId": "={{ $json.opportunityId }}"
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "a273802e-653b-43bb-b4b1-d2f41ee98f35",
"name": "Prevent Duplicate Alerts",
"type": "n8n-nodes-base.if",
"notes": "Suppresses duplicate alerts within 24h cooldown. Proceeds if cooldown expired OR risk worsened.",
"position": [
9072,
2800
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "check_should_alert",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.shouldSendAlert }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "d78845ff-ee3a-4f37-b4c6-569535b93da2",
"name": "Send Risk Alert to Slack Channel",
"type": "n8n-nodes-base.slack",
"notes": "Sends structured Slack block with all risk details, SF opportunity link, run ID, email signals, and confidence score.",
"position": [
9280,
2608
],
"parameters": {
"text": "= {{ $json.riskLevel.toUpperCase() }} RISK: {{ $json.opportunityName }}\n\n ${{ ($json.dealAmount ?? 0).toLocaleString() }} | {{ $json.accountName }} | {{ $json.ownerName }}\n {{ $json.stageName }} | Closes {{ $json.closeDate }} ({{ $json.daysUntilClose }} days)\n\n Score: {{ $json.riskScore }}/100 | {{ $json.estimatedDealHealth }} | {{ $json.confidenceScore }}% confidence\n {{ $json.topRiskDriver }}\n\n Stalled {{ $json.daysSinceLastActivity }} days | Silent {{ $json.silenceGapDays }} days | {{ $json.customerRepliesFound }} replies\n\n {{ $json.riskSummary.substring(0, 200) }}{{ $json.riskSummary.length > 200 ? '...' : '' }}\n\nNext: {{ $json.rawRecommendedActionsArray[0] }}\n\n <{{ $json.opportunityLink }}|Open in Salesforce> | Run {{ $json.workflowRunId.substring($json.workflowRunId.lastIndexOf('-')+1) }}\n{{ $json.urgencyFlag ? ' ACT NOW \u2014 Risk escalating' : '' }}",
"select": "channel",
"blocksUi": {
"blocksValues": [
{
"type": "header",
"textUi": {
"text": "={{ ' Deal Risk Alert \u2014 ' + $json.riskLevel + ' Risk: ' + $json.opportunityName.substring(0, 60) }}",
"emoji": true
}
},
{
"type": "section",
"textUi": {
"text": "={{ '*Opportunity:* ' + $json.opportunityName + '\\n*Account:* ' + $json.accountName + '\\n*Owner:* ' + $json.ownerName + '\\n*Stage:* ' + $json.stageName + '\\n*Deal Amount:* $' + ($json.dealAmount ?? 0).toLocaleString() + '\\n*Close Date:* ' + $json.closeDate + ' (' + $json.daysUntilClose + ' days remaining)\\n*Days Since Last Activity:* ' + $json.daysSinceLastActivity + '\\n*Silence Gap (Email):* ' + $json.silenceGapDays + ' days\\n*External Participants:* ' + $json.uniqueExternalParticipants + '\\n*Customer Replies Found:* ' + $json.customerRepliesFound }}"
}
},
{
"type": "divider"
},
{
"type": "section",
"textUi": {
"text": "={{ '*Risk Score:* ' + $json.riskScore + '/100 | *Risk Level:* ' + $json.riskLevel + ' | *Deal Health:* ' + $json.estimatedDealHealth + ' | *Confidence:* ' + $json.confidenceScore + '%\\n\\n*Top Risk Driver:*\\n' + $json.topRiskDriver + '\\n\\n*Risk Summary:*\\n' + ($json.riskSummary ?? '').substring(0, 600) + '\\n\\n*Key Risk Factors:*\\n' + $json.rawRiskFactorsArray.slice(0,5).map((f, i) => (i+1) + '. ' + f).join('\\n') + '\\n\\n*Recommended Actions:*\\n' + $json.rawRecommendedActionsArray.slice(0,5).map((a, i) => (i+1) + '. ' + a).join('\\n') }}"
}
},
{
"type": "section",
"textUi": {
"text": "={{ '<' + $json.opportunityLink + '| Open Opportunity in Salesforce>' }}"
}
},
{
"type": "context",
"elementsUi": {
"elementsValues": [
{
"text": "={{ 'Run ID: ' + $json.workflowRunId + ' | Assessed: ' + $json.workflowRunDate.substring(0,10) + ' | Emails analyzed: ' + $json.totalEmailsAnalyzed + ' | Urgency: ' + ($json.urgencyFlag ? ' Immediate Action Required' : ' Monitor Closely') }}",
"type": "mrkdwn"
}
]
}
}
]
},
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0ATD2VAH3K",
"cachedResultName": "all-akn8ndemo"
},
"messageType": "block",
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3,
"continueOnFail": true
},
{
"id": "09b8844a-0281-4ebf-bbfd-dc1efb3319f4",
"name": "Check if Deal is Critical Risk",
"type": "n8n-nodes-base.if",
"notes": "Only escalates Critical-level deals to leadership. Prevents alert fatigue.",
"position": [
9520,
2672
],
"parameters": {
"options": {},
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "condition_002",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.riskLevel }}",
"rightValue": "Critical"
}
]
}
},
"typeVersion": 2
},
{
"id": "869c8245-52dc-41d9-b9e4-6296ea592cd0",
"name": "Non-Critical \u2014 No Email Escalation",
"type": "n8n-nodes-base.noOp",
"position": [
9728,
2768
],
"parameters": {},
"typeVersion": 1
},
{
"id": "d2d22fc3-d081-42cf-9d68-b2c807251d09",
"name": "Send Suppressed Alert Notification to Owner",
"type": "n8n-nodes-base.gmail",
"notes": "IMPROVED: Now sends a proper subject line and HTML body. Sends to deal owner (not hardcoded personal email). Keeps owner informed even when Slack alert is suppressed.",
"position": [
9280,
2912
],
"parameters": {
"sendTo": "={{ $json.ownerEmail }}",
"message": "={{ '<p>Hi ' + $json.ownerName + ',</p><p>A risk alert for <strong>' + $json.opportunityName + '</strong> was suppressed because an alert was already sent within the last 24 hours and the risk level has not worsened.</p><p><strong>Current Risk Level:</strong> ' + $json.riskLevel + ' (' + $json.riskScore + '/100)</p><p><a href=\"' + $json.opportunityLink + '\">View in Salesforce</a></p><small>Run ID: ' + $json.workflowRunId + '</small>' }}",
"options": {},
"subject": "={{ ' Deal Alert Suppressed (Cooldown Active) \u2014 ' + $json.opportunityName }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2,
"continueOnFail": true
},
{
"id": "10d3a2f4-7d1f-4659-ac8b-d6efbbcdb77b",
"name": "Auto Trigger: Run Deal Risk Check (Every 6 Hours)",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "Triggers deal risk assessment every 6 hours. Adjust interval as needed for your sales cycle.",
"position": [
5792,
2960
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"typeVersion": 1.2
},
{
"id": "38a218e6-a3b0-4efe-a45d-ba6644629ee2",
"name": "Fetch Open Deals from Salesforce",
"type": "n8n-nodes-base.httpRequest",
"notes": "Uses env var SALESFORCE_INSTANCE_URL. Set this in n8n environment variables. Retries up to 3 times on failure.",
"position": [
6016,
2960
],
"parameters": {
"url": "=={{$env.SALESFORCE_INSTANCE_URL}}/services/data/v59.0/query",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "SELECT Id, Name, AccountId, Account.Name, OwnerId, Owner.Name, Owner.Email, StageName, Amount, CloseDate, LastActivityDate, LastModifiedDate, CreatedDate, Probability, Description, LeadSource, ForecastCategory FROM Opportunity WHERE IsClosed = FALSE ORDER BY LastModifiedDate DESC LIMIT 100"
}
]
},
"nodeCredentialType": "salesforceOAuth2Api"
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "1d356e37-1f8d-471b-93e8-83d0bac88c95",
"name": "Check: Salesforce API Error",
"type": "n8n-nodes-base.if",
"notes": "Guards against Salesforce API auth failures or empty responses before processing.",
"position": [
6240,
2960
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "check_sf_error",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.error !== undefined || ($json.records === undefined && $json.errorCode !== undefined) }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "384c9b2e-3464-41f4-9859-379dbd55eba9",
"name": "Slack Alert: Salesforce Fetch Failed",
"type": "n8n-nodes-base.slack",
"notes": "NEW: Surfaces Salesforce fetch failures to Slack instead of silently failing.",
"position": [
6464,
2864
],
"parameters": {
"text": " Salesforce API Failure \u2014 Deal Risk Run Aborted",
"select": "channel",
"blocksUi": {
"blocksValues": [
{
"type": "header",
"textUi": {
"text": " Salesforce API Failure \u2014 Deal Risk Run Aborted",
"emoji": true
}
},
{
"type": "section",
"textUi": {
"text": "={{ '*Error:* ' + JSON.stringify($json.error ?? $json) + '\\n*Time:* ' + new Date().toISOString() + '\\n*Action:* Check Salesforce OAuth credentials and API limits.' }}"
}
}
]
},
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0AS8TV8KSP",
"cachedResultName": "all-n8ntask"
},
"messageType": "block",
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3,
"continueOnFail": true
},
{
"id": "9b9b80b9-74ac-4750-9e3d-10f425a3d386",
"name": "Transform Salesforce Data \u2192 Deal List",
"type": "n8n-nodes-base.code",
"notes": "Unpacks records[] from Salesforce REST response into individual items and strips metadata attributes.",
"position": [
6464,
3056
],
"parameters": {
"jsCode": "const body = $input.item.json;\nconst records = body.records ?? [];\n\nif (records.length === 0) {\n console.warn('[WARN] Salesforce returned 0 open opportunities. Check your org data or SOQL filter.');\n return [];\n}\n\nreturn records.map(r => {\n const { attributes, ...rest } = r;\n if (rest.Account && rest.Account.attributes) delete rest.Account.attributes;\n if (rest.Owner && rest.Owner.attributes) delete rest.Owner.attributes;\n return { json: rest };\n});"
},
"typeVersion": 2
},
{
"id": "4836471b-092b-4beb-aadd-db375e5e41f0",
"name": "Email Alert: Critical Deal (Leadership)",
"type": "n8n-nodes-base.gmail",
"position": [
9728,
2576
],
"parameters": {
"sendTo": "={{ $env.LEADERSHIP_EMAIL ?? 'leadership@yourcompany.com' }}",
"message": "={{ ' CRITICAL: Deal at Immediate Risk \u2014 ' + $json.opportunityName + ' ($' + ($json.dealAmount ?? 0).toLocaleString() + ')' }}",
"options": {},
"subject": "Deal Risk"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "6d219f51-52f6-4e48-b068-08eb083d5e7b",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
4544,
1856
],
"parameters": {
"width": 800,
"height": 1040,
"content": "## AI Deal Risk Monitoring Workflow\n\nAutomates deal risk detection using Salesforce data, email signals, AI analysis, and Slack/Email alerts.\n\n## Flow:\n\nSchedule Trigger \u2192 Fetch Deals \u2192 Check Errors\n\n\u2192 Clean & Validate Data\n\n\u2192 Missing Data \u2192 Slack Alert\n\n\u2192 Valid Data \u2192 Smart Triage\n\n\u2192 Healthy \u2192 Update CRM\n\n\u2192 Risky \u2192 Fetch Emails \u2192 Merge \u2192 Generate Insights\n\n\u2192 AI Risk Analysis \u2192 Validate Output\n\n\u2192 Low Risk \u2192 Update CRM\n\n\u2192 Medium/High/Critical \u2192 Update CRM \u2192 Check Cooldown\n\n\u2192 Alert \u2192 Slack Notification\n\n\u2192 Critical \u2192 Leadership Email\n\u2192 Suppressed \u2192 Notify Owner\n\n## Setup Steps\nConfigure schedule (e.g., every 6 hours)\nConnect: Salesforce, Gmail, Outlook, Slack, AI\nEnsure Salesforce risk fields exist\nSet Slack channels for alerts\n\n## How It Works\n\nThe workflow fetches open deals, cleans and validates data, and filters only important deals for AI analysis.\n\nIt enriches deals with email activity (engagement, replies, inactivity) and uses AI to generate a risk score, insights, and actions.\n\nResults are saved to Salesforce and alerts are sent via Slack.\nCritical deals trigger leadership emails, while a cooldown prevents duplicate alerts."
},
"typeVersion": 1
},
{
"id": "c3a33750-359e-46a3-b3f7-2d995d08cc5e",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
5728,
2544
],
"parameters": {
"color": 7,
"width": 672,
"height": 864,
"content": "## Deal Risk Check Trigger\n\nAutomatically triggers the workflow on a schedule, fetches open deals from Salesforce, and validates API responses to ensure smooth risk analysis processing and early error detection."
},
"typeVersion": 1
},
{
"id": "58746adf-1788-49c1-a8a8-539c7777a77a",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
6432,
2544
],
"parameters": {
"color": 7,
"width": 832,
"height": 864,
"content": "## Deal Data Preparation\n\nTransforms Salesforce deal data into a structured list, cleans and standardizes key fields, and checks for missing information to ensure the dataset is ready for accurate risk analysis.\n"
},
"typeVersion": 1
},
{
"id": "a6d55177-bbe7-4aed-a1ce-c7c697e29a47",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
7312,
3232
],
"parameters": {
"color": 7,
"width": 672,
"height": 304,
"content": "\n\n\n\n\n\n\n\n\n\n\n\n\n## Incomplete Deal Handling\n\nRoutes deals with missing data for manual review and sends Slack alerts to notify about incomplete records, ensuring no critical issues are overlooked bef"
},
"typeVersion": 1
},
{
"id": "bbe45990-7f01-40c0-a751-b15fde713139",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
7312,
2432
],
"parameters": {
"color": 7,
"width": 672,
"height": 784,
"content": "## Deal Communication Aggregation\n\nFilters high-risk deals, collects related emails from Outlook and Gmail, and merges all communication data to provide complete context for deeper risk evaluation."
},
"typeVersion": 1
},
{
"id": "279c281d-5780-4f8c-9d91-021f0ff24d2e",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
8032,
2432
],
"parameters": {
"color": 7,
"width": 928,
"height": 784,
"content": "## AI-Powered Deal Risk Evaluation Pipeline\n\nAggregates email insights, analyzes deal risk using AI, validates outputs, and determines whether the risk level is high enough to trigger action.\n"
},
"typeVersion": 1
},
{
"id": "f0e6be56-9718-47a8-a72a-5474917edc1b",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
9008,
2432
],
"parameters": {
"color": 7,
"width": 464,
"height": 784,
"content": "## Intelligent Deal Risk Action & Notification\nUpdates CRM with risk status, filters out duplicate alerts, and intelligently routes notifications\u2014sending critical alerts to Slack while notifying owners of suppressed or low-risk deals."
},
"typeVersion": 1
},
{
"id": "8d0f00cb-d196-4998-b410-ba441f2dfdbb",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
9504,
2432
],
"parameters": {
"color": 7,
"width": 512,
"height": 784,
"content": "## Critical Risk Escalation Handling\nEvaluates whether a deal meets critical risk criteria and triggers immediate email alerts for urgent cases, while safely ignoring non-critical deals to avoid unnecessary escalation."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "4c1f6335-a6d2-45ac-8e8d-e58774197be5",
"connections": {
"Combine All Email Data": {
"main": [
[
{
"node": "Prepare Email Insights for AI",
"type": "main",
"index": 0
}
]
]
},
"Prevent Duplicate Alerts": {
"main": [
[
{
"node": "Send Risk Alert to Slack Channel",
"type": "main",
"index": 0
},
{
"node": "Check if Deal is Critical Risk",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Suppressed Alert Notification to Owner",
"type": "main",
"index": 0
}
]
]
},
"Clean & Prepare Deal Data": {
"main": [
[
{
"node": "Standardize Deal Fields (Set Node)",
"type": "main",
"index": 0
}
]
]
},
"Analyze Deal Risk Using AI": {
"main": [
[
{
"node": "Validate & Clean AI Results",
"type": "main",
"index": 0
}
]
]
},
"Get Deal Emails from Gmail": {
"main": [
[
{
"node": "Combine All Email Data",
"type": "main",
"index": 0
}
]
]
},
"Check: Salesforce API Error": {
"main": [
[
{
"node": "Slack Alert: Salesforce Fetch Failed",
"type": "main",
"index": 0
}
],
[
{
"node": "Transform Salesforce Data \u2192 Deal List",
"type": "main",
"index": 0
}
]
]
},
"Validate & Clean AI Results": {
"main": [
[
{
"node": "Is This Deal Risky Enough to Act?",
"type": "main",
"index": 0
}
]
]
},
"Get Deal Emails from Outlook": {
"main": [
[
{
"node": "Combine All Email Data",
"type": "main",
"index": 0
}
]
]
},
"Prepare Email Insights for AI": {
"main": [
[
{
"node": "Analyze Deal Risk Using AI",
"type": "main",
"index": 0
}
]
]
},
"Check if Deal is Critical Risk": {
"main": [
[
{
"node": "Email Alert: Critical Deal (Leadership)",
"type": "main",
"index": 0
}
],
[
{
"node": "Non-Critical \u2014 No Email Escalation",
"type": "main",
"index": 0
}
]
]
},
"Fetch Open Deals from Salesforce": {
"main": [
[
{
"node": "Check: Salesforce API Error",
"type": "main",
"index": 0
}
]
]
},
"Send Risk Alert to Slack Channel": {
"main": [
[
{
"node": "Check if Deal is Critical Risk",
"type": "main",
"index": 0
}
]
]
},
"Is This Deal Risky Enough to Act?": {
"main": [
[
{
"node": "Update Deal Risk in Salesforce",
"type": "main",
"index": 0
},
{
"node": "Prevent Duplicate Alerts",
"type": "main",
"index": 0
}
],
[
{
"node": "Save Low-Risk Deal Status",
"type": "main",
"index": 0
}
]
]
},
"Check for Missing Deal Information": {
"main": [
[
{
"node": "Send Incomplete Deals for Manual Review",
"type": "main",
"index": 0
}
],
[
{
"node": "Filter Important Deals (Risky / High Value)",
"type": "main",
"index": 0
}
]
]
},
"Standardize Deal Fields (Set Node)": {
"main": [
[
{
"node": "Check for Missing Deal Information",
"type": "main",
"index": 0
}
]
]
},
"Send Incomplete Deals for Manual Review": {
"main": [
[
{
"node": "Alert Slack on Incomplete Deal Data",
"type": "main",
"index": 0
}
]
]
},
"Transform Salesforce Data \u2192 Deal List": {
"main": [
[
{
"node": "Clean & Prepare Deal Data",
"type": "main",
"index": 0
}
]
]
},
"Filter Important Deals (Risky / High Value)": {
"main": [
[
{
"node": "Get Deal Emails from Gmail",
"type": "main",
"index": 0
},
{
"node": "Get Deal Emails from Outlook",
"type": "main",
"index": 0
}
],
[
{
"node": "Mark Deal as Healthy (Skip AI Analysis)",
"type": "main",
"index": 0
}
]
]
},
"Auto Trigger: Run Deal Risk Check (Every 6 Hours)": {
"main": [
[
{
"node": "Fetch Open Deals from Salesforce",
"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.
gmailOAuth2slackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs every six hours to pull open Salesforce opportunities, enriches selected deals with Gmail and Microsoft Outlook email context, uses OpenAI to score deal risk, writes the assessment back to Salesforce, and notifies teams in Slack with optional email escalation…
Source: https://n8n.io/workflows/16444/ — 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.
This workflow is an automated invoice payment tracking and reminder system for the Polish accounting service iFirma.pl. It monitors unpaid and overdue invoices, then automatically sends escalating rem
This workflow runs on a schedule to monitor HubSpot deals with upcoming contract expiry dates. It filters deals that are 30, 60, or 90 days away from expiration and processes each one individually. Ba
This workflow identifies HubSpot deals that have gone untouched for 21+ days and automatically updates their status to Closed Lost. It fetches associated contacts, retrieves their details, and sends p
This workflow runs daily to pull cloud spend from a billing API, compare it to a Google Sheets rolling baseline, and alert on cost spikes by creating a Jira incident, posting to Slack, emailing Financ
This workflow automatically monitors solar energy production every 2 hours by fetching data from the Energidataservice API. If the energy output falls below a predefined threshold, it instantly notifi