AutomationFlowsAI & RAG › Automate Stale Deal Follow-ups in Salesforce with Gpt-5.1, Email, Slack & Tasks

Automate Stale Deal Follow-ups in Salesforce with Gpt-5.1, Email, Slack & Tasks

ByLe Nguyen @leeseifer on n8n.io

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…

Cron / scheduled trigger★★★★☆ complexityAI-powered16 nodesSalesforceOpenAIHTTP Request ToolHTTP RequestEmail SendSlack
AI & RAG Trigger: Cron / scheduled Nodes: 16 Complexity: ★★★★☆ AI nodes: yes Added:

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 →

Download .json
{
  "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.

Pro

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 →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

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

Twilio, OpenAI, Email Send +2
AI & RAG

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

HTTP Request, OpenAI, Postgres +2
AI & RAG

Automatically detects missed Zoom demos booked via Calendly and triggers AI-powered follow-up sequences.

HTTP Request, OpenAI, Email Send +3
AI & RAG

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

HTTP Request, Google Sheets, OpenAI +3
AI & RAG

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

Google Sheets, OpenAI, HTTP Request +2