たまには開発者っぽい記事を書こうということで今回は
ApexでSlack通知を実装する
を書きたいと思いますが、時代はAIですので
チャッピー(ChatGPT)にほぼ丸投げしました。
今年もSalesforceのアドベントカレンダーに参加です。
この記事に関連してTriggerエラーの時にAPIコールする方法も書く予定です(*'ω'*)
チャッピーに要件を投げる
投げた内容は下記のとおり(今回はサンプルの業務イメージ)
Salesforceでシステム開発しています。Slack通知をApexで実装する方法を教えてください。
通知するタイミングは商談のフェーズがClosed Wonになったときです。
通知する内容は商談が成約したこと、取引先名、商談名、商談所有者でお願いします。
すぐに回答を出してくれます。長々と続くので手順を切りながら解説します。
手順のとおり進めていく
①SlackでWebhooks URLを作る
Slackのアプリ管理から「Incoming Webhooks」をインストールして

②Salesforce に Remote Site 設定を行う
リモートサイトの設定でWebhook URLを設定する。ドメインまででいいかと

③Apexクラス(通知クラス)を作成
public class SlackNotifier {
@future(callout=true)
public static void sendOpportunityWonNotification(Id oppId) {
Opportunity opp = [
SELECT Id, Name, StageName,
Account.Name,
Owner.Name
FROM Opportunity
WHERE Id = :oppId
];
// Slackメッセージ(好きに整形可)
String message = '🎉 *商談が成約しました!*\n'
+ '• *取引先*: ' + opp.Account.Name + '\n'
+ '• *商談名*: ' + opp.Name + '\n'
+ '• *商談所有者*: ' + opp.Owner.Name;
// Slack Incoming Webhook
String webhookUrl = 'https://hooks.slack.com/services/xxxx/yyyy/zzzz';
HttpRequest req = new HttpRequest();
req.setEndpoint(webhookUrl);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
// Slackに送るJSON
Map<String,Object> payload = new Map<String,Object>();
payload.put('text', message);
req.setBody(JSON.serialize(payload));
Http http = new Http();
http.send(req);
}
}
このままでコンパイルできました。
webhookUrlの値は控えていたものに差し替えてください。
※カスタム設定かカスタムメタデータに外出ししたほういいですが、今回は割愛
④ Opportunity Trigger を作成
trigger OpportunityTrigger on Opportunity (after update) {
for (Opportunity opp : Trigger.new) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
// フェーズがClosed Wonになった時のみ通知
if (opp.StageName == 'Closed Won' &&
oldOpp.StageName != 'Closed Won') {
SlackNotifier.sendOpportunityWonNotification(opp.Id);
}
}
}
たぶんこのままでも動きますが、、
気になるところを指摘
OpportunityTriggerから直接SlackNotifierを呼ぶのではなく、トリガハンドラを介するようにしてほしいのと、update時だけではなくinsert時にも動くようにしてほしいです。
trigger OpportunityTrigger on Opportunity (after insert, after update) {
if (Trigger.isAfter) {
if (Trigger.isInsert) {
OpportunityTriggerHandler.afterInsert(Trigger.new);
}
if (Trigger.isUpdate) {
OpportunityTriggerHandler.afterUpdate(Trigger.new, Trigger.oldMap);
}
}
}
public class OpportunityTriggerHandler {
public static void afterInsert(List<Opportunity> newList) {
List<Id> notifyIds = new List<Id>();
for (Opportunity opp : newList) {
// Insert 時点で Closed Won なら通知
if (opp.StageName == 'Closed Won') {
notifyIds.add(opp.Id);
}
}
for (Id oppId : notifyIds) {
SlackNotifier.sendOpportunityWonNotification(oppId);
}
}
public static void afterUpdate(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
List<Id> notifyIds = new List<Id>();
for (Opportunity opp : newList) {
Opportunity oldOpp = oldMap.get(opp.Id);
// Closed Won に“変わった”時のみ通知
if (opp.StageName == 'Closed Won' &&
oldOpp.StageName != 'Closed Won') {
notifyIds.add(opp.Id);
}
}
for (Id oppId : notifyIds) {
SlackNotifier.sendOpportunityWonNotification(oppId);
}
}
}
これもそのままでコンパイルとおりました。
動作確認
Insert時にClosed Won

Update時にClosed Won

これらを保存すると、、
Slackに通知来ました!!!
※取引先を紐づけるの忘れてましたのでnullになってます、、
ついでにテストクラスも

とやったんですが
出力されたテストクラスはテスト実行でNGになったのでいい感じにしておきました。
これでテストも成功してカバレッジもクリアします。
@isTest
private class OpportunityTriggerTest {
// --- 共通 Slack Mock ---
private class SlackMock implements HttpCalloutMock {
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('ok');
return res;
}
}
// --- 共通テストデータ ---
@testSetup
static void setupData() {
Test.setMock(HttpCalloutMock.class, new SlackMock());
Account acc = new Account(Name = 'Test Account');
insert acc;
// Update 用(最初は Prospecting)
Opportunity oppUpdate = new Opportunity(
Name = 'Opp Update Test',
StageName = 'Prospecting',
CloseDate = Date.today(),
AccountId = acc.Id
);
insert oppUpdate;
// Stage 変更なし(Closed Won → Closed Won)
Opportunity oppNoChange = new Opportunity(
Name = 'Opp No Change',
StageName = 'Closed Won',
CloseDate = Date.today(),
AccountId = acc.Id
);
insert oppNoChange;
}
@isTest
static void testInsertClosedWon() {
Test.setMock(HttpCalloutMock.class, new SlackMock());
Account acc = [
SELECT Id FROM Account
WHERE Name = 'Test Account'
LIMIT 1
];
Opportunity oppInsert = new Opportunity(
Name = 'Opp Insert ClosedWon',
StageName = 'Closed Won',
CloseDate = Date.today(),
AccountId = acc.Id
);
Test.startTest();
insert oppInsert;
Test.stopTest();
System.assert(true);
}
@isTest
static void testUpdateToClosedWon() {
Test.setMock(HttpCalloutMock.class, new SlackMock());
Opportunity opp = [
SELECT Id, StageName FROM Opportunity
WHERE Name = 'Opp Update Test'
LIMIT 1
];
opp.StageName = 'Closed Won';
Test.startTest();
update opp;
Test.stopTest();
System.assert(true);
}
@isTest
static void testDoNotNotifyWhenStageDoesNotChange() {
Test.setMock(HttpCalloutMock.class, new SlackMock());
Opportunity opp = [
SELECT Id, StageName FROM Opportunity
WHERE Name = 'Opp No Change'
LIMIT 1
];
// StageName は変更しない
opp.Name = 'Opp No Change Updated';
Test.startTest();
update opp;
Test.stopTest();
System.assert(true);
}
}
@isTest
private class SlackNotifierTest {
// --- Slack Mock ---
private class SlackMock implements HttpCalloutMock {
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('ok');
return res;
}
}
// --- 共通テストデータ ---
@testSetup
static void setupData() {
Account acc = new Account(Name = 'Notifier Test Account');
insert acc;
Opportunity opp = new Opportunity(
Name = 'Notifier Test Opp',
StageName = 'Value Proposition',
CloseDate = Date.today(),
AccountId = acc.Id
);
insert opp;
}
@isTest
static void testSendOpportunityWonNotification() {
Test.setMock(HttpCalloutMock.class, new SlackMock());
Opportunity opp = [
SELECT Id FROM Opportunity
WHERE Name = 'Notifier Test Opp'
LIMIT 1
];
Test.startTest();
SlackNotifier.sendOpportunityWonNotification(opp.Id);
Test.stopTest();
System.assert(true);
}
}
チャッピーとの会話
最期らへんグダってますが、全文を共有します。
途中でチャッピーからも提案ありますが、トリガなので複数件想定で本来は実装するべきです。今回は1件(〜数件程度)の作成・更新を前提としたものとさせてください。
公開期限は2025年内で🎅
あとがき
AIが賢すぎて、なんかもう開発者いらねぇなぁと思う最近です。
自分も捨てられないように、PMスキルの向上と最新情報のキャッチアップは怠らないようにしていきたいです。
アドベントカレンダーもう1記事書く予定なのでそちらもお楽しみに(^_-)-☆








