外部サービス連携

取引先責任者の作成をSlackスレッドへ返信する実装

chat.postMessageで親メッセージを投稿し、返ってきたtsの値を使って子メッセージをスレッド返信するところまでを説明します。

2023.11.10

シナリオ

取引先を作成すると、Slackの指定チャンネルへその内容を投稿します。続けてその取引先に取引先責任者を作成すると、さきほど投稿した親メッセージのスレッドへ返信を投稿します。1件ずつ独立した投稿にするのではなく、親子関係のあるレコードの通知を、Slack上でも親子のスレッドとしてまとめます。

取引先の作成からSlack投稿、取引先責任者の作成、スレッド投稿までをつないだ流れ図
処理の流れです。取引先の投稿で得た値を保持し、取引先責任者の投稿を同じスレッドへ返す順序を見てください。
レコード作成から3つのApexクラスへ処理が渡る構成図。右端がSlackClient
クラスの構成です。レコード作成を受ける入口のクラスから、共通のクライアントへ渡る流れを見てください。

先に確かめること 標準の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__cBotトークン(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で送ります。

動作確認

  1. 取引先を作成する

    名前を付けて保存します。フローが起動し、Slackの指定チャンネルへ親メッセージが投稿されます。取引先の PostTS__c にtsが保存されていることを確認します。

  2. その取引先に取引先責任者を作成する

    姓を付けて保存します。フローが起動し、1で投稿された親メッセージのスレッドへ返信が投稿されます。

  3. Slack側で確認する

    親メッセージにスレッド返信が連なっていることを、チャンネルの表示で確認します。

Slackのスレッド画面。親メッセージの下に1件の返信が連なっている
動作確認の結果です。親メッセージの下に「1件の返信」として子の投稿が連なっている点を見てください。

開発者コンソールの匿名実行から直接動作を確かめる場合は、レコードを作成したうえで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との連携まで承ります。状況を伺ったうえで、進め方をご提案します。