取引先責任者の作成をSlackスレッドへ返信する実装
chat.postMessageで親メッセージを投稿し、返ってきたtsの値を使って子メッセージをスレッド返信するところまでを説明します。
シナリオ
取引先を作成すると、Slackの指定チャンネルへその内容を投稿します。続けてその取引先に取引先責任者を作成すると、さきほど投稿した親メッセージのスレッドへ返信を投稿します。1件ずつ独立した投稿にするのではなく、親子関係のあるレコードの通知を、Slack上でも親子のスレッドとしてまとめます。
先に確かめること 標準のSlack連携で足りないか
SalesforceにはSlackと連携する標準機能があり、レコードとチャンネルを結び付ける使い方が用意されています。単に「作成したら投稿する」だけなら、まず標準機能で足りないかを確認してください。
本記事が扱う「親のメッセージに対して、後から作られる子レコードをスレッド返信する」という組み方は、標準機能の範囲を超えるため自作します。
使うAPIとトークンの置き場所
Incoming Webhookは投稿専用で、投稿したメッセージの ts(タイムスタンプ)を受け取れません。スレッド返信には親メッセージの ts が要るため、ここでは Slack の Web API chat.postMessage を使います。chat.postMessage はレスポンスに投稿した ts を含みます。
認証にはBotトークンが要ります。値はコードに直書きせず、カスタム設定(階層型)に保存します。
| 項目 | 保存先 | 用途 |
|---|---|---|
SlackChannel__c | カスタム設定 SlackSetting__c | 投稿先チャンネルID |
SlackToken__c | カスタム設定 SlackSetting__c | Botトークン(Authorization: Bearer で送信) |
⚠️ Botトークンは秘密情報です。画面から編集できるカスタム設定に置く場合は、参照・編集できるプロファイルを絞ってください。エンドポイントも合わせて外に出したい場合は、名前付き認証情報でトークンごと管理する方法もあります。
親 取引先の作成をSlackへ投稿する
取引先が作成されたら、レコードトリガーフローからApexアクションを呼び出します。コールアウトはトリガー由来の同期処理から直接は行えないため、@future(callout=true) で非同期に実行します。
public with sharing class PostAccountToSlack { @InvocableMethod(label='Post Account to Slack' callout=true) public static void execute(List<Id> accountIds) { for (Id accId : accountIds) { SlackService.postAccount(accId); } } }
フローから呼び出すApexアクションです。取引先Idを受け取り、非同期メソッドへ渡すだけです。
public with sharing class SlackService { @future(callout=true) public static void postAccount(Id accId) { Account acc = [SELECT Name FROM Account WHERE Id = :accId]; SlackSetting__c setting = SlackSetting__c.getOrgDefaults(); String channel = setting.SlackChannel__c; if (String.isBlank(channel)) { return; } Map<String, Object> payload = new Map<String, Object>{ 'channel' => channel, 'text' => '取引先が登録されました: ' + acc.Name }; HttpResponse res = new SlackClient().postMessage(JSON.serialize(payload)); if (res.getStatusCode() != 200) { return; } Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(res.getBody()); String ts = (String) body.get('ts'); if (String.isNotBlank(ts)) { update new Account(Id = accId, PostTS__c = ts); } } }
投稿本文を組み立ててchat.postMessageを呼び、返ってきたtsを取引先の項目へ保存します。future(callout=true)なので戻り値は受け取れず、結果はDMLで反映します。
ts は投稿1件ごとに発行される識別子です。このtsを、後から来る子メッセージが thread_ts として指定すると、同じスレッドに連なります。取引先の PostTS__c に保存しておくのはこのためです。
子 取引先責任者の作成をスレッド返信する
取引先責任者が作成されたときも、レコードトリガーフローからApexアクションを呼び出します。親の PostTS__c が入っている場合だけ、thread_ts を付けて投稿します。
public with sharing class PostContactToSlack { @InvocableMethod(label='Post Contact to Slack' callout=true) public static void execute(List<Id> contactIds) { for (Id conId : contactIds) { SlackService.postContact(conId); } } }
フローから呼び出すApexアクションです。取引先責任者Idを受け取ります。
@future(callout=true) public static void postContact(Id conId) { List<Contact> conList = [ SELECT Id, LastName, Account.PostTS__c FROM Contact WHERE Id = :conId AND Account.PostTS__c != null ]; if (conList.isEmpty()) { return; } Contact con = conList[0]; SlackSetting__c setting = SlackSetting__c.getOrgDefaults(); String channel = setting.SlackChannel__c; if (String.isBlank(channel)) { return; } Map<String, Object> payload = new Map<String, Object>{ 'channel' => channel, 'text' => '取引先責任者が登録されました: ' + con.LastName, 'thread_ts' => con.Account.PostTS__c }; new SlackClient().postMessage(JSON.serialize(payload)); }
SlackServiceへ追加するメソッドです。親のtsが無ければ何もしません。
public with sharing class SlackClient { private final String token; private final String endpoint = 'https://slack.com/api/chat.postMessage'; public SlackClient() { SlackSetting__c setting = SlackSetting__c.getOrgDefaults(); token = setting.SlackToken__c; } public HttpResponse postMessage(String body) { HttpRequest req = new HttpRequest(); req.setMethod('POST'); req.setEndpoint(endpoint); req.setHeader('Content-Type', 'application/json'); req.setHeader('Authorization', 'Bearer ' + token); req.setBody(body); return new Http().send(req); } }
chat.postMessage を呼び出す共通クライアントです。トークンはカスタム設定から読み、Bearerで送ります。
動作確認
取引先を作成する
名前を付けて保存します。フローが起動し、Slackの指定チャンネルへ親メッセージが投稿されます。取引先の
PostTS__cにtsが保存されていることを確認します。その取引先に取引先責任者を作成する
姓を付けて保存します。フローが起動し、1で投稿された親メッセージのスレッドへ返信が投稿されます。
Slack側で確認する
親メッセージにスレッド返信が連なっていることを、チャンネルの表示で確認します。
開発者コンソールの匿名実行から直接動作を確かめる場合は、レコードを作成したうえでApexアクションのクラスを呼び出します。
Account acc = new Account(Name = 'test'); insert acc; PostAccountToSlack.execute(new List<Id>{ acc.Id }); Contact con = new Contact(AccountId = acc.Id, LastName = 'test'); insert con; PostContactToSlack.execute(new List<Id>{ con.Id });
匿名実行の例です。レコード作成後にInvocableMethodを直接呼び出しています。
ここで間違えやすい
| 間違い | 何が起きるか |
|---|---|
| トリガーやレコードトリガーフローから同期でコールアウトしようとする | 実行時エラーになります。@future(callout=true) かQueueableに包みます |
| Incoming Webhookでスレッド返信しようとする | Incoming Webhookは投稿専用で ts を受け取れないため、chat.postMessage を使います |
thread_ts に自分自身の ts を入れる | 新しいスレッドが始まってしまいます。親メッセージの ts を渡します |
| Botトークンをコードに直書きする | 変更にデプロイが要り、コードを見た人にトークンが渡ります |
確認した環境
- 2026年9月 / Salesforce Summer '26(API バージョン 67.0)時点の公式ドキュメントで確認しています
- Slackの
chat.postMessageとthread_tsの仕様はdocs.slack.dev(api.slack.comからのリダイレクト先)の公式ドキュメントで確認しています
まとめ
- スレッド返信には
chat.postMessageを使います。Incoming Webhookはtsを返さないため使えません - 親メッセージの
tsを保存しておき、子メッセージのthread_tsに渡すとスレッドに連なります - コールアウトはトリガー由来の同期処理から直接は行えません。
@future(callout=true)かQueueableで非同期にします - Botトークンはカスタム設定や名前付き認証情報に置き、コードへ直書きしません
Salesforceの導入・運用についてご相談ください
導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。