Send a Slack alert when a Salesforce deal changes stage using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code or another agent that supports the Agent Skills standard
  • Salesforce connected app or session token with API access to Opportunities
  • Slack bot with chat:write permission added to the target channel
Environment Variables
# Salesforce instance URL (e.g. https://yourorg.my.salesforce.com)
SALESFORCE_INSTANCE_URL=your_value_here
# Salesforce access token with API access to Opportunity records
SALESFORCE_ACCESS_TOKEN=your_value_here
# Slack bot token with chat:write permission
SLACK_BOT_TOKEN=your_value_here
# Target Slack channel ID for deal stage alerts
SLACK_CHANNEL_ID=your_value_here

Overview

This approach creates an agent skill that queries Salesforce for recently modified opportunities, checks for stage changes, and posts alerts to Slack. Unlike flow-based approaches, this runs on-demand or on a schedule — ideal for a periodic check rather than real-time alerts.

Stage change detection

This script finds opportunities modified in the last hour, which may include changes other than stage transitions. Salesforce doesn't expose field-level change history via SOQL on standard objects without using OpportunityFieldHistory. For exact stage change detection, query OpportunityFieldHistory instead.

Step 1: Create the skill

Create .claude/skills/sf-deal-stage-alerts/SKILL.md:

---
name: sf-deal-stage-alerts
description: Check for recent Salesforce opportunity stage changes and post alerts to Slack
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Check for Salesforce opportunities that changed stage in the last hour and post alerts to Slack.
 
Run: `python $SKILL_DIR/scripts/check_stages.py`

The key settings:

  • disable-model-invocation: true — the skill has external side effects (posting to Slack), so it only runs when you explicitly invoke it with /sf-deal-stage-alerts
  • allowed-tools: Bash(python *) — restricts execution to Python scripts only, preventing unintended shell commands

Step 2: Write the script

Create .claude/skills/sf-deal-stage-alerts/scripts/check_stages.py:

#!/usr/bin/env python3
import os, sys, requests
 
INSTANCE_URL = os.environ.get("SALESFORCE_INSTANCE_URL")
ACCESS_TOKEN = os.environ.get("SALESFORCE_ACCESS_TOKEN")
SLACK_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
SLACK_CHANNEL = os.environ.get("SLACK_CHANNEL_ID")
 
if not all([INSTANCE_URL, ACCESS_TOKEN, SLACK_TOKEN, SLACK_CHANNEL]):
    print("ERROR: Set SALESFORCE_INSTANCE_URL, SALESFORCE_ACCESS_TOKEN, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID")
    sys.exit(1)
 
SF_HEADERS = {
    "Authorization": f"Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
}
 
# Query recently modified opportunities
soql = (
    "SELECT Id, Name, Amount, StageName, LastModifiedDate "
    "FROM Opportunity "
    "WHERE LastModifiedDate > LAST_N_HOURS:1 AND StageName != null"
)
resp = requests.get(
    f"{INSTANCE_URL}/services/data/v59.0/query",
    headers=SF_HEADERS,
    params={"q": soql},
)
resp.raise_for_status()
records = resp.json().get("records", [])
 
if not records:
    print("No opportunities modified in the last hour")
    sys.exit(0)
 
# Post to Slack
from slack_sdk import WebClient
slack = WebClient(token=SLACK_TOKEN)
 
for opp in records:
    amount = float(opp.get("Amount") or 0)
    opp_url = f"{INSTANCE_URL}/{opp['Id']}"
 
    slack.chat_postMessage(
        channel=SLACK_CHANNEL,
        text=f"Deal updated: {opp['Name']}",
        blocks=[
            {"type": "section", "text": {"type": "mrkdwn",
                "text": f"🔄 *Deal Stage Changed*\n*{opp['Name']}* is in *{opp['StageName']}*\nAmount: ${amount:,.0f}"}},
            {"type": "context", "elements": [{"type": "mrkdwn",
                "text": f"<{opp_url}|View in Salesforce>"}]}
        ]
    )
 
print(f"Posted {len(records)} deal alerts to Slack")

Troubleshooting

What the script does

  1. Validates environment — checks that SALESFORCE_INSTANCE_URL, SALESFORCE_ACCESS_TOKEN, SLACK_BOT_TOKEN, and SLACK_CHANNEL_ID are set, exits with a clear error if any are missing
  2. Queries Salesforce — runs a SOQL query via the REST API (/services/data/v59.0/query) to find Opportunities modified in the last hour that have a stage set
  3. Formats each alert — builds a Block Kit message per opportunity with the deal name, current stage, amount, and a deep link to the Salesforce record
  4. Posts to Slack — sends one chat_postMessage per opportunity via the Slack SDK and logs the total number of alerts posted

Step 3: Run it

/sf-deal-stage-alerts

Step 4: Schedule (optional)

For hourly checks, schedule via Cowork or cron:

# crontab — run every hour
0 * * * * cd /path/to/project && python .claude/skills/sf-deal-stage-alerts/scripts/check_stages.py

When to use this approach

  • You want periodic digest-style alerts, not real-time
  • You don't want to maintain Salesforce flows or Connected Apps for n8n
  • You want to run checks on demand during pipeline reviews

Cost

  • No Claude API calls — this skill uses direct API calls only
  • Salesforce and Slack API usage is included in their respective plans

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.