This workflow corresponds to n8n.io template #11157 — we link there as the canonical source.
This workflow follows the Emailsend → 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "e9d9200c-3696-4f76-b04b-9472c205a39a",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-1936,
-976
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1.2
},
{
"id": "62cf078e-066a-492d-9882-c8dac3ea5baf",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"position": [
-1728,
-976
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "2790e2d6-bf5f-4d3a-a02a-705afd3b1caf",
"name": "stale_days",
"type": "number",
"value": 14
}
]
}
},
"typeVersion": 3.4
},
{
"id": "8a0f4a2f-9339-4de1-a614-e00ef8298139",
"name": "Perform a query",
"type": "n8n-nodes-base.salesforce",
"position": [
-1488,
-976
],
"parameters": {
"query": "=Select id from opportunity where Stage_Unchanged_Days__c = {{ $json.stale_days }} And StageName Not In ('Closed Won', 'Closed Lost')",
"resource": "search"
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "7ba41db7-0f84-4cd3-8a19-b0ea640601b5",
"name": "Get an opportunity",
"type": "n8n-nodes-base.salesforce",
"position": [
-1264,
-976
],
"parameters": {
"resource": "opportunity",
"operation": "get",
"opportunityId": "={{ $json.Id }}"
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "75389940-46a3-4512-aca9-b8c21bab5f9b",
"name": "Message a model",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-1040,
-976
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-5.1",
"cachedResultName": "GPT-5.1"
},
"options": {},
"responses": {
"values": [
{
"role": "system",
"content": "=You are a Salesforce Sales Follow-Up Assistant.\n\nYour job:\n- When an Opportunity stage has not changed for some time, draft:\n 1) A follow-up EMAIL to the client\n 2) A short SMS to the client\n 3) A concise SLACK message to the internal team\n\nYou will receive:\n- A Salesforce Opportunity record as JSON (called `opportunity`).\n{{ JSON.stringify($json) }}\n- Key fields include (but are not limited to):\n - Id, Name, AccountId, StageName, Stage_Unchanged_Days__c,\n Amount, CloseDate, Type, LeadSource, OwnerId,\n MainCompetitors__c, OrderNumber__c, IsClosed, IsWon.\n\nYou have access to the following tool:\n- query_soql(soql: string)\n - Executes a SOQL query and returns a list of records as JSON\n with the fields specified in the SELECT clause.\n\nTOOL USAGE \u2013 ALWAYS DO THESE QUERIES\n\n1) Fetch Opportunity Notes\n- Goal: personalize messages with recent context / pain points / last interactions.\n- Call `query_soql` with a SOQL that fetches notes on this Opportunity, for example:\n - SELECT Id, Title, Body\n FROM Note\n WHERE ParentId = '{opportunity.Id}'\n ORDER BY LastModifiedDate DESC\n LIMIT 5\n- Use the returned notes (Title and Body) to understand:\n - recent discussions,\n - customer concerns,\n - agreed next steps,\n - any specific products or competitors mentioned.\n\n2) Fetch Primary Contact on the Account\n- Goal: address the right person and use email/phone where possible.\n- Call `query_soql` with a SOQL that fetches ONE contact for the Opportunity\u2019s Account that has an Email, for example:\n - SELECT Id, Name, Email, Phone\n FROM Contact\n WHERE AccountId = '{opportunity.AccountId}'\n AND Email != null\n ORDER BY LastModifiedDate DESC\n LIMIT 1\n- Treat the returned Contact (if any) as the **primary contact**.\n- Use:\n - Contact.Name to personalize greeting (\u201cHi <Name>,\u201d).\n - Contact.Email as the assumed receiver of the email.\n - Contact.Phone for SMS (if Phone is null, still write an SMS template as if sent to their mobile).\n- If no contact is returned:\n - Use a generic salutation like \u201cHi there,\u201d\n - Still draft the email and SMS text as templates.\n\n3) Fetch Opportunity Owner (for Signature and Slack)\n- Goal: include the owner\u2019s name (and optionally email) in the email signature and Slack mentions.\n- Call `query_soql` with a SOQL that returns the Owner user, for example:\n - SELECT Id, Name, Email\n FROM User\n WHERE Id = '{opportunity.OwnerId}'\n LIMIT 1\n- Use:\n - Owner.Name in the email signature and in the Slack message.\n - Owner.Email in the signature if available.\n - Owner.Id (or opportunity.OwnerId) as the Task OwnerId.\n- If no owner record is returned, you can:\n - Still use opportunity.OwnerId for the Task OwnerId field.\n - Sign the email as \u201cYour account manager\u201d without a name.\n\nYou should:\n- Perform up to THREE calls to `query_soql` (Notes, Contact, Owner) at the beginning.\n- Incorporate the returned data into your reasoning before drafting any messages.\n- If a query returns no rows, handle it gracefully and write more generic content.\n\nCOMMUNICATION LOGIC\n\n- This assistant is triggered because `Stage_Unchanged_Days__c` indicates the stage has not changed for some time.\n- Adapt the messaging based on `StageName` and status flags:\n - If the Opportunity is still open (IsClosed = false):\n - Use a polite, value-driven nudge to move things forward.\n - Reference any relevant notes or previous conversations.\n - If the Opportunity is Closed Won (IsClosed = true AND IsWon = true):\n - Treat it as a post-sale check-in, onboarding, or upsell touch.\n - Confirm everything is going smoothly and offer next steps.\n - If the Opportunity is Closed Lost (IsClosed = true AND IsWon = false):\n - Keep it short: thank them, request brief feedback if appropriate, and keep the door open for future opportunities.\n- Refer to the delay naturally (\u201cover the past few weeks\u201d, \u201crecently\u201d) instead of exact day counts,\n unless referencing Stage_Unchanged_Days__c is clearly helpful.\n\nSTYLE GUIDELINES\n\nEMAIL:\n- Audience: the primary Contact (if found) or generic client.\n- Tone: professional, friendly, concise.\n- Structure:\n - Greeting: use Contact.Name if available; otherwise \u201cHi there,\u201d.\n - Intro: who you are + why you\u2019re reaching out (reference Opportunity Name in natural language:\n e.g., \u201cyour order for mobile generators\u201d).\n - Context: mention that things have been quiet / unchanged for a while, referencing notes if possible.\n - Value: 1\u20133 sentences focusing on next steps, clarifying questions, or how you can help.\n - CTA: clear and simple (e.g., propose a time to talk, ask for confirmation/feedback).\n - Signature:\n - Include Owner.Name (and Owner.Email if available).\n - Include company name or generic role (\u201cSales Team\u201d) if needed.\n- Do NOT invent discounts, promises, or contractual commitments that are not mentioned.\n\nSMS:\n- Very short and to the point.\n- Assume they know who you/your company are (add a short identifier like \u201c\u2013 <Owner Name>, <Company>\u201d).\n- Use a clear CTA (e.g., \u201cReply if you\u2019d like to schedule a quick call this week.\u201d).\n- If Contact.Phone is missing, still generate an SMS template as if it will be sent to the right number later.\n\nSLACK (INTERNAL TEAM):\n- Target: Opportunity Owner and sales / deals channel.\n- Include:\n - Opportunity Name, StageName, Amount, CloseDate, Stage_Unchanged_Days__c.\n - Account/Contact name (if available).\n - Short summary of key insights from Notes.\n - Short summary of the angle of the email/SMS (no need to paste the full text).\n- Recommend the next internal action (e.g., \u201ccall the client\u201d, \u201creview pricing\u201d, \u201cconfirm delivery timeline\u201d).\n- Mention the Owner by name; if your environment supports @mentions, format text in a way that could be used\n\nTASK (for Owner):\n- Purpose: ensure there is a clear owner action in Salesforce linked to this Opportunity.\n- The Task should:\n - Be assigned to the Opportunity Owner (OwnerId = opportunity.OwnerId or Owner.Id).\n - Be related to the Opportunity (WhatId = opportunity.Id).\n - Have a clear Subject describing the follow-up (e.g., \u201cFollow up on stalled opportunity \u2013 <Opportunity Name>\u201d).\n - Have a Description summarizing:\n - Why the task was created (stage unchanged),\n - Key context from notes,\n - The gist of the email/SMS that will be sent.\n - Use reasonable default fields:\n - Status: \"Not Started\"\n - Priority: \"Normal\"\n - DueDate can be set conceptually to \u201ctoday\u201d or \u201ctomorrow\u201d as a short text description in the Description,\nbut leave it out of the api_body if you are not given a date field.\n\nOUTPUT FORMAT\n\nReturn ONLY a JSON object with this structure (no extra text, no markdown):\n\n{\n \"email\": {\n \"to\":\"email\"\n \"subject\": \"string\",\n \"body\": \"string\"\n },\n \"sms\": {\n \"to\":\"Phone\"\n \"body\": \"string\"\n },\n \"slack\": {\n \"channel_hint\": \"string\",\n \"message\": \"string\"\n },\n \"task\": {\n \"subject\": \"string\",\n \"description\": \"string\",\n \"status\": \"string\",\n \"priority\": \"string\",\n \"api_body\": {\n \"Subject\": \"string\",\n \"Description\": \"string\",\n \"Status\": \"string\",\n \"Priority\": \"string\",\n \"OwnerId\": \"string\",\n \"WhatId\": \"string\"\n }\n}\n\n- email.subject: clear and concise.\n- email.body: multi-paragraph email using \"\\n\\n\" for paragraph breaks.\n- sms.body: 1\u20133 sentences max.\n- slack.channel_hint: short hint to help route this message (e.g., \"#sales\", \"@OwnerName\", \"#opportunity-followup\").\n- slack.message: a concise message summarizing the situation and internal recommendation.\n\nREASONING\n\nBefore you generate the JSON:\n- Think through:\n - What is the current stage and has it stalled?\n - What do the Notes say about last interactions and customer concerns?\n - Who is the primary Contact and how to address them?\n - Who is the Owner and how to include them in signature and Slack?\n - What is the most reasonable, low-friction next step for the client?\n - What does the internal team need to know and do?\n\nThen, generate the JSON output EXACTLY in the specified schema.\n"
}
]
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "29cfe789-91e7-48ef-8f5f-b7e15275b883",
"name": "query_soql",
"type": "n8n-nodes-base.httpRequestTool",
"position": [
-896,
-736
],
"parameters": {
"url": "https://[mydomain].salesforce.com/services/data/v64.0/query/",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('parameters0_Value', ``, 'string') }}"
}
]
},
"toolDescription": "Use to query opportunity notes ",
"nodeCredentialType": "salesforceOAuth2Api"
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.3
},
{
"id": "33b39d65-3f0e-4f4d-ba59-6770bd21a831",
"name": "Create Task",
"type": "n8n-nodes-base.httpRequest",
"position": [
-336,
-720
],
"parameters": {
"url": "https://[mydomain ].salesforce.com/services/data/v60.0/sobjects/Task",
"method": "POST",
"options": {},
"jsonBody": "={{ $json.task.api_body }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "salesforceOAuth2Api"
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "cd24d4f9-9d47-4994-9b0a-26253ec5f7cd",
"name": "Parse JSON",
"type": "n8n-nodes-base.code",
"position": [
-720,
-976
],
"parameters": {
"jsCode": "return JSON.parse($input.first().json.output[0].content[0].text)"
},
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "7f5a791d-7a0e-42ec-b46e-599aa85c458b",
"name": "Send Email SMTP Customer",
"type": "n8n-nodes-base.emailSend",
"position": [
-336,
-1312
],
"parameters": {
"text": "={{ $json.email.body }}",
"options": {},
"subject": "={{ $json.email.subject }}",
"toEmail": "={{ $json.email.to }}",
"fromEmail": "=from@email.com",
"emailFormat": "text"
},
"credentials": {
"smtp": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "c9998bd2-6222-45b0-917e-b9cb1a7948b1",
"name": "Send Message To Internal Team",
"type": "n8n-nodes-base.slack",
"position": [
-336,
-1024
],
"parameters": {
"text": "={{ $('Parse JSON').item.json.slack.message }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "ChannelID"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "850f5ec2-aa83-404f-be88-dbdec5524889",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2336,
-1600
],
"parameters": {
"width": 320,
"height": 368,
"content": "**Stale Opportunity Follow-Up \u2013 Email, Slack & Task**\n\nThis flow automatically follows up on Salesforce Opportunities whose stage hasn\u2019t changed for a configured number of days. Each morning at 8:00, it finds \u201cstale\u201d Opportunities, enriches them with Notes, Contact and Owner data, and sends a personalized follow-up email to the client plus a Slack summary to the internal team. It also creates a Salesforce Task for the Opportunity Owner so every stalled deal has a clear next action and is tracked inside Salesforce.\n"
},
"typeVersion": 1
},
{
"id": "8e038257-2fab-47c1-9765-5ae9f43db833",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2336,
-1200
],
"parameters": {
"width": 320,
"height": 832,
"content": "**Setup & Testing Instructions**\n\n1. Create the `Stage_Unchanged_Days__c` field on Opportunity:\n - Type: Formula (Number, 0 decimal places) \n - Formula: \n `IF(ISBLANK(LastStageChangeDate), TODAY() - DATEVALUE(CreatedDate), TODAY() - DATEVALUE(LastStageChangeDate))` \n This calculates how many days the Opportunity has been in its current stage (or since creation if `LastStageChangeDate` is blank) and is what the flow uses to detect \u201cstale\u201d deals.\n\n2. Configure credentials: make sure your Salesforce OAuth2, OpenAI API, SMTP and Slack credentials are all valid and selected in their nodes (Salesforce nodes, `query_soql`, `Create Task`, `Send Email SMTP Customer`, Slack node). \n\n3. Adjust logic: set `stale_days` in the **Edit Fields** node to the number of days you consider a stage to be stalled (e.g. 7, 14). Optionally, refine the Salesforce query in **Perform a query** to match your pipeline rules. \n\n4. Test safely: disable the **Schedule Trigger**, then run the flow manually on a small set of test Opportunities. Check the AI output in **Message a model** and **Parse JSON**, confirm Tasks are created in Salesforce, and verify the test email and Slack message look correct before re-enabling the schedule for production.\n"
},
"typeVersion": 1
},
{
"id": "42812b94-f828-4e1c-b7fa-befaa60352b0",
"name": "Sticky Note \u2013 Actions Summary",
"type": "n8n-nodes-base.stickyNote",
"position": [
-448,
-1424
],
"parameters": {
"color": 7,
"width": 736,
"height": 896,
"content": "## Send Email (SMTP)\nUses `email.to`, `email.subject`, `email.body` to email the customer.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## Slack \u2013 Internal Update\nPosts a short stale-opportunity summary to the selected Slack channel.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## Create Salesforce Task\nUses `task.api_body` to create a follow-up Task linked to the Opportunity.\n"
},
"typeVersion": 1
},
{
"id": "eb1c4494-2e32-4dd3-915f-49395db2694f",
"name": "Sticky Note \u2013 AI Engine1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1088,
-1168
],
"parameters": {
"color": 7,
"width": 544,
"height": 640,
"content": "## AI Follow-Up Engine\nCalls the model with Opportunity data and the `query_soql` tool, then parses JSON into `email`, `sms`, `slack`, and `task.api_body`. Central brain and guardrail between AI and Salesforce, fetching live CRM data for smarter, safer follow-up automation.\n"
},
"typeVersion": 1
},
{
"id": "bcbb9e81-302e-4370-8bea-fd2389ab23af",
"name": "Sticky Note \u2013 Schedule & Threshold",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1984,
-1168
],
"parameters": {
"color": 7,
"width": 400,
"height": 416,
"content": "## Schedule & Stale Threshold\nRuns the flow on a schedule (default 8:00) and sets `stale_days` for the SOQL filter. Adjust the schedule or `stale_days` value here to match your follow-up policy.\n"
},
"typeVersion": 1
},
{
"id": "fd995303-b70e-4534-a038-70da75dbf451",
"name": "Sticky Note \u2013 Opportunity Fetch",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1568,
-1168
],
"parameters": {
"color": 7,
"width": 464,
"height": 416,
"content": "## Salesforce Opportunity Fetch\nQueries Salesforce for open Opportunities whose stage is unchanged for `stale_days`, excluding closed deals, then loads full Opportunity details for each match. This defines which deals are \u201cstale\u201d and provides rich context for AI follow-up.\n"
},
"typeVersion": 1
}
],
"connections": {
"Parse JSON": {
"main": [
[
{
"node": "Send Email SMTP Customer",
"type": "main",
"index": 0
},
{
"node": "Create Task",
"type": "main",
"index": 0
},
{
"node": "Send Message To Internal Team",
"type": "main",
"index": 0
}
]
]
},
"query_soql": {
"ai_tool": [
[
{
"node": "Message a model",
"type": "ai_tool",
"index": 0
}
]
]
},
"Create Task": {
"main": [
[]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Perform a query",
"type": "main",
"index": 0
}
]
]
},
"Message a model": {
"main": [
[
{
"node": "Parse JSON",
"type": "main",
"index": 0
}
]
]
},
"Perform a query": {
"main": [
[
{
"node": "Get an opportunity",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Get an opportunity": {
"main": [
[
{
"node": "Message a model",
"type": "main",
"index": 0
}
]
]
},
"Send Email SMTP Customer": {
"main": [
[]
]
}
}
}
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.
openAiApisalesforceOAuth2ApislackApismtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Runs every morning at 8:00 using the Schedule Trigger. Sets a value and queries Salesforce for Opportunities where equals that value and the stage is not Closed Won / Closed Lost. For each “stale” Opportunity, loads full deal details and sends them to an OpenAI model. The model…
Source: https://n8n.io/workflows/11157/ — 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.
Fetch campaign & members from Salesforce. GPT‑4 auto‑writes a channel‑appropriate, personalised outbound message. Switch node sends via Twilio (SMS/WhatsApp), SMTP (Email). Mark each member as process
A scheduled process aggregates content from eight distinct data sources and standardizes all inputs into a unified format. AI models perform sentiment scoring, detect conspiracy or misinformation sign
Automatically detects missed Zoom demos booked via Calendly and triggers AI-powered follow-up sequences.
Imagine a dedicated financial expert tirelessly working behind the scenes, sifting through every transaction, every investment move, and every accounting entry. That's exactly what this automated syst
Automate your social media content pipeline from idea to scheduled post. This workflow reads content ideas from a Google Sheet, uses OpenAI to generate platform-optimized posts for LinkedIn, X (Twitte