TeamSpirit の稟議が通ったら Slack へ。通知でなく担当を決めるボタンにした

契約の稟議(社内の申請・承認)が承認された。では、誰が契約書を送るのか。ある会社(約90名)では、そこが決まっていなかった。案件の担当者が Slack に「稟議完了」と書いてから、法務が動く。法務は2人いる。2人とも気づいて電子契約を作れば、同じ契約書が先方に2通届く。

法務から相談を受けて、仕組みを作った。TeamSpirit(社内の申請・承認を回すサービス)で稟議が承認されたら、それを合図に Slack に通知を出す。通知の[対応する]を押した人が、その契約の担当になる。画面の設定だけでほぼ作れた。ただ、TeamSpirit から Slack に送る1か所だけは、最初に試した形では Slack に受け付けてもらえず、そこだけ短いプログラムにした。

先に断っておく。本番には入れて動かしているが、通知先はまだテスト用のチャンネルで、法務の人にはまだ使ってもらっていない。事実は2026年11月10日の時点のものだ。設定の名前やプログラムは、後半の「担当者向け」にまとめた。

この記事でわかること

  • ▶ 承認を知らせるだけでは、2人で同じ契約書を送るのは防げない理由と、ボタンで担当を決める形
  • ▶ 通知に載せたもの・載せなかったもの(金額は載せない)
  • ▶ 「コードなしで」のつもりが1か所だけプログラムになった経緯と、どこまでをプログラムにしたか
  • ▶ 作る前に決めておくこと(受け口のアドレスの扱い・作業用の権限・持ち主と止め方)と、自分の会社で確かめること

▼作った理由:契約の稟議は月5〜11件。承認のあと、誰が送るかが決まっていない

契約の稟議が承認されたあと、次に誰が動くかが決まっていなかった。法務から聞いた今の流れは次のとおりだ。

法務のチェック → 稟議の申請・承認 → 契約書の発送(紙か電子契約)→ 契約書に署名する役員が署名 → 先方が署名 → 締結

この会社では、契約の稟議の承認は月に5〜11件ある(2026年7〜10月)。困りごとは2つあった。

  • ▶ 承認を知るのが、担当者の投稿待ち:案件の担当者が「稟議完了」と書くまで、法務は発送に入れない。一手間かかっている
  • ▶ 法務の2人のどちらが動くか決まっていない:2人とも電子契約を作り、2通送ってしまうことが起こりうる

やりたいことは3つに絞った。承認を合図に法務が発送に入れること。2人の作業が重ならないこと。役員への署名の依頼まで、同じ流れの中で出せることだ。

▼仕組み:承認 → Slack に通知 → [対応する]を押した人が担当

この会社の稟議は TeamSpirit(勤怠と社内の申請・承認をまとめて扱うサービス。Salesforce という業務システムの土台の上で動く)で回っている。稟議が承認済みになると、Salesforce の自動処理が Slack の「ワークフロー」に知らせる。ワークフローは、Slack の中でボタンやフォームをコードなしで組める機能だ。

Slack に出るものと、押す人は次の4段になる。

段 Slack に出るもの 押す人
1 専用のチャンネルに通知(件名・会社名・押印の種類・申請者・稟議へのリンク)+[対応する] 法務のどちらか
2 スレッドに「◯◯ が対応中です」+[署名依頼] 担当になった人
3 フォームが開き、URL を貼る → スレッドで役員に署名をお願いするメンション(件名・押印の種類・貼った URL)+担当者に締結が済んだら押すよう頼む一文+[締結完了] 担当になった人
4 先方の署名が済んだら[締結完了]→ スレッドに「締結完了(◯◯)」 担当になった人(法務)

通知に載せたもの・載せなかったもの

載せた 載せなかった
件名、会社名、押印の種類(電子契約か紙か。発送のしかたが分かる)、申請者、稟議へのリンク 金額、法務の指摘を書く欄、区分、契約期間

契約の中身は Slack に流さない。発送に入るのに要るものだけを載せ、中身はリンクから稟議を開いて見る。区分は、依頼者(法務の1人)が要らないと決めた。

最後のボタンは「対応完了」ではなく「締結完了」

最後のボタンの名前は「締結完了」にした。「対応完了」だと、署名を終えた役員が自分の対応が済んだと思って押しかねない。押すのは先方の署名まで見届けた法務の担当者なので、担当者にメンションして、締結が済んだら押すよう頼む文面にした。

押したあとの表示も役に立つ。自分の画面では、[対応する]を押すと、元の通知のボタンが「誰が押したか」の表示に変わった。スレッドを開かなくても、誰が持ったか分かる。ただ、ほかの人の画面でも同じように変わるかは、まだ確かめていない。変わらなければ、2人目も[対応する]を押せてしまうかもしれない。本番のチャンネルで法務の2人に押してもらって確かめる。表示の一部(「clicked」)は英語のままで、変えられるかも分かっていない。

▼判断の分かれ目:欲しいのは通知ではなく「誰が持ったか」が残るボタン。すでにある連携は使わなかった

相談の入り口は、承認の通知だった。目的を聞くと、2人の作業が重ならないようにしたい、ということだった。通知が届くだけなら、2人とも見て、2人とも動く。知らせるだけでは、重複は防げない。要るのは、誰が持ったかが残ることだった。

この会社の Salesforce には、別の用途で Slack に投稿する連携がすでにあった。前例どおりに作れば早い。ただ、その連携が出すのは普通のメッセージで、ボタンは付かない。TeamSpirit 自身にもチャットとの連携の機能が入っていたが、使われていなかった。

そこで、通知は Slack のワークフローで受けることにした。ボタンが付けられ、Slack 側はコードなしで組める。作った自分がいなくなっても、会社の人が文面やボタンを直せる。

あわせて、やらないことも2つ決めた。

  • ▶ Slack で押した結果を、Salesforce に書き戻さない:書き戻すと作りが重くなり、TeamSpirit の製品側の部品を触ることになる。誰が持ったか・いつ締結したかは、Slack のスレッドに残る
  • ▶ 法務の既存のチャンネルのやりとりに、直接は書き込まない:法務は契約ごとのやりとりを別のチャンネルでしている。そこへの書き込みはワークフローだけではできず、別のプログラムが要る。代わりに専用のチャンネルに通知し、3段目のフォームに URL を貼ってもらう形にした。貼るのは、法務のチャンネルのその契約のやりとりへのリンクに決めた。依頼者からは「流れを法務チャンネルに戻せる」と了承をもらった

「通知が欲しい」と言われたら、通知のあと誰が何をするかまで聞く。担当を決めたいなら、通知ではなくボタンが要る。ここが一番の分かれ目だった。

▼一番引っかかったこと:試したコードなしの送り方は Slack に受け付けられず、送る部分だけプログラムにした

最初は、Salesforce から Slack への送信も、プログラムを書かずに作った。Salesforce にある、設定だけで外に送る仕組み(外部サービス)を使った。ところが、試験環境(本番の設定を写した練習用の環境)で稟議を承認しても、Slack に通知が届かなかった。

調べると、Slack が「その場所は移った」という返事(301)を返して、受け取っていなかった。外部サービスは、送り先を「土台のアドレス」と「その後ろに付ける部分」に分けて書く決まりで、後ろの部分は「/」で始める。土台のほうにアドレスを丸ごと入れたので、最後に「/」が1文字付く形になった。これが原因だと判断した。同じアドレスでも、最後に「/」を付けずに手元から送ったときは、Slack は受け付けていた。

試したのは1つの形だけだ。受け口のアドレス(Slack のワークフローが外から通知を受け取る専用のアドレス)を丸ごと、Salesforce の中の送り先をしまっておく場所(土台のアドレス)に入れる形だ。アドレスを前半と後半に分けて設定すれば「/」は付かないはずだが、試していない。分けると、鍵にあたるアドレスの後半が、しまっておく場所の外に残ってしまう(アドレスを鍵と同じに扱う理由は、後の「始める前に決めておくこと」で書く)。

そこで、送る部分だけを Apex(Salesforce の中で動くプログラム)にした。40行ほどと、動きを確かめるテストが2つだ。どの稟議で通知を出すかの条件と、通知に載せる値の組み立ては、画面の設定のまま残した。プログラムは、直す人が読める量にとどめたかった。

「コードなしで作ってほしい」と頼んでも、相手のサービスがその送り方を受け付けるかは、試すまで分からない。1か所だけプログラムになったら、どこまでをプログラムにするかを決める。そして、会社のシステムにプログラムが増えることは、会社に説明が要る。この会社には、進捗の共有で伝えた。

ほかにも細かいつまずきが2つあった(通知の申請者の欄が空になった件と、Slack 側で受け取る値の名前に日本語が使えない件)。どちらも後半の担当者向けに書いた。

▼かかった期間:相談から本番まで8日。組んだのは2日

日付(2026年) やったこと
11/2法務から相談。TeamSpirit 側の設定を読んで、どこで承認済みになるか、既存の連携が何かを調べた
11/4「ボタン付きの Slack ワークフロー」に決めて、法務に流れを返した。作業用の権限を準備
11/6依頼者と、通知に載せるもの・ボタンの流れを詰めた
11/9Slack 側を組んで、ダミーの通知で最後のボタンまで動くことを確認
11/9〜10試験環境で Salesforce 側を組み、承認から通知まで通した(301 の切り分けもここ)
11/10本番に入れて有効化(通知先はテスト用のチャンネル)

試験環境には、本番と同じ承認ルートが入っていた。データは空で、メールも止まっていた。だから役員に頼まなくても、自分で申請から承認まで通して試せた。

▼始める前に決めておくこと:受け口のアドレスの置き場所・作業用の権限・持ち主と止め方

受け口のアドレスは鍵と同じ。置き場所は1つにする

Slack のワークフローには、外から通知を受け取る専用のアドレス(受け口のアドレス)がある。このアドレスを知っていれば、誰でもそのチャンネルに通知を流せる。だから鍵と同じに扱う。

手順書やチャットには書かない。Salesforce の中の、外への送り先をしまっておく場所にだけ置く。プログラムにも、画面の設定にも書かない。差し替えるときは、その1か所を変えるだけで済む。

作業用の権限は、足すものを1つに絞って伝える

自分のアカウントには、その「しまっておく場所」を作る権限がなかった。管理者の権限をまるごと広げてもらう代わりに、その権限だけを入れた作業用の権限のセットを作り、自分に割り当てた。足したことは、進捗の共有で依頼者に伝えた。作業が終わったら外す。外部の立場で自分の権限を足せるなら、何を足したか・いつ外すかを先に決めて伝える。

持ち主と止め方

  • ▶ 持ち主:Slack のワークフローの共同管理者に、法務の2人などを入れる(まだ入れていない)。作った自分の契約が切れても止まらないようにする
  • ▶ 止め方は2つ:Salesforce 側の自動処理を無効にする。または、Slack のワークフローを非公開にする
  • ▶ 失敗しても承認は止まらない:通知は承認のあとに別で動く。Slack が受け付けないときは、Salesforce が数時間のあいだ自動でやり直す(試験環境では4回やり直した)

▼本番の入れ方:通知先をテスト用のまま、本番に入れた

最後に確かめたいのは、実際の承認で動くかだ。試験用の稟議を本番で通すと、承認者である役員に、意味のない承認をお願いすることになる。

そこで、本番に入れて有効にし、通知先だけをテスト用のチャンネルにした。実際の契約の稟議が承認されると、テスト用のチャンネルに通知が届く。月5〜11件あるので、待てば本物で確かめられる。

本番に入れたあと、2つやった。テスト用のチャンネルを非公開にした(本物の件名と会社名が載るため)。本番に入れたことを依頼者に伝え、了承をもらった。

通知が出てはいけない場面は、試験環境で3つ確かめた。別の種類の稟議の承認、契約の稟議の却下、承認済みの稟議の保存し直しだ。どれも通知は出なかった。本番からの試しの送信は Slack に届いている。本物の承認での通知は、次の承認を待っている。

▼まだ終わっていないこと

  • ▶ 本番のチャンネルとメンバーを決め、通知先を切り替える:依頼者と決める。法務の人には、まだ使ってもらっていない
  • ▶ まだ確かめていないこと:役員へのメンションが届くか(テスト用では名前をただの文字にしている)/ほかの人の画面でもボタンの表示が変わるか/役員が承認したときに権限が足りるか/稟議へのリンクが本番で正しく開くか/失敗したときの知らせのメールが誰に届くか/代わりに稟議を作る運用があると、申請者の欄に別の人の名前が出ないか/署名する役員が法務のチャンネルに入っているか(入っていないと、貼ったリンクを開けない)
  • ▶ 持ち主を入れる:Slack のワークフローの共同管理者に、法務の2人などを入れる
  • ▶ 法務に渡す運用メモ:ボタンの意味、持ち主、止め方。稟議へのリンクが入った通知には、Slack が本人にだけ「Salesforce に接続しますか?」と出すが、押さなくていいこと
  • ▶ 後片付け:作業用の権限を外す。テスト用のワークフローを消す

▼まとめ:自分の会社で確かめること

稟議や申請の承認を、チャットで回したい会社は、担当者(または委託先)に次を確かめてもらえばいい。

  • ▶ 承認のあと、誰が次に動くかが決まっているか:「誰かが見て動く」になっていないか
  • ▶ 通知を頼むとき、通知のあと誰が何をするかまで決めたか:担当を決めたいなら、ボタンが要る
  • ▶ 通知に、契約の中身(金額など)を流していないか:載せるのは動くのに要るものだけ。中身はリンクから見る
  • ▶ 作った人がいなくなったら、誰が止め方を知っているか:共同管理者と、止め方のメモがあるか
  • ▶ 受け口のアドレスを、手順書やチャットに貼っていないか:置き場所は1つにする

▼担当者向け:設定と手順の詳細

ここからは、実際に設定する人向けに、正式な名前と手順を書く。環境は Salesforce の Enterprise Edition、フローの API バージョンは 62.0、2026年11月時点。

前半の言い方と、実際の名前

前半の言い方 実際の名前
稟議が承認済みになる稟議オブジェクト teamspirit__AtkApply__c のステータス teamspirit__Status__c が「承認済み」になる(承認プロセスの項目自動更新 teamspirit__AtkApplySetApproved で入る)
Salesforce の自動処理レコードトリガーフロー(更新時・保存後・非同期パス)
コードなしで外部に送る仕組み外部サービス(External Services)。フローの HTTP 呼び出しの裏で使われる仕組み。今回はメタデータ API で登録
外への送り先をしまっておく場所指定ログイン情報(Named Credential)。従来の形・認証なし
作業用の権限のセット権限セット(システム権限「指定ログイン情報の管理」だけ)
受け口のアドレスワークフロービルダーの Webhook トリガー(https://hooks.slack.com/triggers/...)
送る部分だけのプログラムApex の呼び出し可能メソッド(@InvocableMethod)

フローの開始条件

  • ▶ オブジェクトとタイミング:稟議(teamspirit__AtkApply__c)、レコードの更新時、保存後
  • ▶ 条件:ステータスが「承認済み」に変わったときだけ(「レコードが更新されて条件を満たしたときのみ」)AND 稟議の種別の項目=契約の値。この会社の種別の値は「番号+全角スペース+名前」の形(例:04. 契約)で、1文字違うと条件に合わない。承認ルートの条件と同じ話は同じ会社の TeamSpirit の記事にも書いた
  • ▶ 承認プロセスの項目自動更新でも動いた:TeamSpirit の承認プロセスは最終承認でステータスを項目自動更新で「承認済み」にする。これでレコードトリガーフローが動くことを試験環境で確かめた
  • ▶ 外を呼ぶのは非同期パス:保存後のフローから外部を呼ぶので、非同期パス(Run Asynchronously)に置いた。非同期パスは、承認した人のユーザーで動いた(試験では自分が承認者)
  • ▶ 稟議の URL:数式で LEFT({!$Api.Partner_Server_URL_620}, FIND("/services", {!$Api.Partner_Server_URL_620}) - 1) & "/" & {!$Record.Id}。本番で正しく開くかは未確認
  • ▶ 空の項目:「(未入力)」にして送る

301 になった形(外部サービス)

301 で失敗したのは、次の形だ。

  • ▶ 指定ログイン情報:従来の形(Legacy)、認証なし(ID の種類「匿名」、認証プロトコル「認証なし」)。URL 欄に Webhook の URL を丸ごと(https://hooks.slack.com/triggers/XXXX/XXXX/XXXX の形)
  • ▶ 外部サービス:フロービルダーの画面ではなく、メタデータ API で登録した(ExternalServiceRegistration、OpenAPI 3.0.1 の仕様。上の指定ログイン情報を参照。パスは /、操作は POST、本文は5つのキーを持つ JSON)
  • ▶ 呼び方:フローの非同期パスから、その外部サービスのアクションを呼ぶ
  • ▶ 結果:フローは失敗。フローの実行記録のエラーは「Moved Permanently」。Apex の匿名実行から同じ外部サービスを呼ぶと、応答の例外の responseCode が 301
  • ▶ 比べたもの:同じ Webhook の URL に、手元の curl で末尾に / を付けずに JSON を POST すると 200 で、通知が出た

OpenAPI では、パスは / で始める決まりになっている(OpenAPI 3.0.1 の Paths Object)。URL を丸ごと指定ログイン情報に入れたのでパスは / だけになり、送り先は「指定ログイン情報の URL + /」になる。Slack の Webhook は末尾に / が付いた URL に 301 を返し、Salesforce 側はそれを失敗として扱った、と判断している。根拠は、パスが / だったこと、301 が返ったこと、末尾に / のない curl は 200 だったことの3つで、送られた URL そのものはログで見ていない。なお、フロービルダーで HTTP 呼び出しを作ると外部サービスが自動で作られる、と Salesforce の管理者向けブログにある(Salesforce Admins Blog)。ただ、画面でパスを空にできるかは確かめていないので、画面から作っても同じことが起きるとは言えない。次のことは試していない。

  • ▶ 末尾に / を付けた URL に curl で送り、301 が返るか
  • ▶ 実際に送られた URL を、デバッグログで目で見ること
  • ▶ フロービルダーの「HTTP 呼び出しを作成」の画面から作った場合(裏で外部サービスとして保存されると理解しているが、同じ結果になるかは試していない)
  • ▶ 指定ログイン情報を https://hooks.slack.com までにして、/triggers/... を外部サービスのパスに書く形(末尾の / は付かないはずだが、Webhook の URL の後半が外部サービスの定義に残る)

外部サービスで作っていたときに気づいたことが2つある。外部サービスの名前は、アンダースコアを入れると「英字で始まり、英数字のみを含む名前を入力してください」で登録できなかった。また、キーのアンダースコアは、生成されるクラスの項目名では x5f に置き換わる(imprint_type が imprintx5ftype になる)。

送る部分だけの Apex

Slack の Webhook には、Apex の HttpRequest で callout: +指定ログイン情報の名前に POST する。URL はコードに書かない。名前は一般化し、空の値を「(未入力)」に置き換える部分は省いて載せる。

public with sharing class SlackWebhookNotify { public class Request { @InvocableVariable(required=true) public String subject; @InvocableVariable public String company; @InvocableVariable public String imprintType; @InvocableVariable public String applicant; @InvocableVariable(required=true) public String approvalUrl; } @InvocableMethod(label=’Slack に通知’ callout=true) public static void send(List<Request> requests) { for (Request r : requests) { HttpRequest req = new HttpRequest(); req.setEndpoint(‘callout:Slack_Webhook’); // URL は指定ログイン情報にだけ置く req.setMethod(‘POST’); req.setHeader(‘Content-Type’, ‘application/json’); req.setTimeout(20000); req.setBody(JSON.serialize(new Map<String, String>{ ‘subject’ => r.subject, ‘company’ => r.company, ‘imprint_type’ => r.imprintType, ‘applicant’ => r.applicant, ‘approval_url’ => r.approvalUrl })); HttpResponse res = new Http().send(req); if (res.getStatusCode() != 200) { throw new CalloutException(‘Slack への通知に失敗(HTTP ‘ + res.getStatusCode() + ‘)’); } } } }
  • ▶ callout=true:外部を呼ぶ呼び出し可能メソッドに付ける
  • ▶ 200 以外は例外にする:フローが失敗扱いになり、実行記録に残る。黙って失敗させない
  • ▶ キーは Slack のワークフローの変数と同じ名前:subject・company・imprint_type・applicant・approval_url

テストは2つ。送るキーが合っているかと、Slack が 301 を返したら例外になるかだ。後者の要点は次のとおり。

@IsTest static void failsOnNon200() { Test.setMock(HttpCalloutMock.class, new MockResponse(301)); // 301 を返すモック Test.startTest(); try { SlackWebhookNotify.send(new List<SlackWebhookNotify.Request>{ request() }); System.assert(false, ‘例外が出るはず’); } catch (CalloutException e) { System.assert(e.getMessage().contains(‘301’), e.getMessage()); } Test.stopTest(); }

ほかに引っかかったこと

  • ▶ 非同期パスで申請者の名前が空になった:最初は $Record.CreatedBy.Name を渡していた。試験環境で承認すると、Slack の通知の申請者が「(未入力)」だった。フローに「レコードを取得」を足し、User を Id = $Record.CreatedById で1件取って、その Name を渡す形に直した。直したあと、申請者の名前が入ることを確かめた
  • ▶ Slack の Webhook の変数のキーに日本語が使えない:画面で試すと、使えたのは英字・数字・ハイフン・アンダースコアだけだった。変数の型はすべてテキストにした
  • ▶ 申請者は Slack のメンションにしていない:Salesforce の側から Slack のユーザーを特定する情報が取れないので、名前の文字だけを出している

Slack のワークフロー

  • ▶ 起動:Webhook。変数は5つ(すべてテキスト)
  • ▶ ステップ:チャンネルにメッセージ+ボタン[対応する]→ スレッドに返信(「{押した人} が対応中です」)+ボタン[署名依頼]→ フォーム(法務のチャンネルのやりとりへのリンクを貼る)→ スレッドに返信(役員へのメンション+貼った URL、担当者へのお願い)+ボタン[締結完了]→ スレッドに返信(「締結完了({押した人})」)
  • ▶ 返信先:どのスレッドへの返信も、ステップ1のメッセージに返す
  • ▶ Webhook 起動でも、ボタンのあとにフォームを開けることを確かめた
  • ▶ 表示の細かい点:スレッドのボタンを押すと「{押した人} さんが「*署名依頼*」をクリックしました」のように、アスタリスクが付いたまま出た
  • ▶ 持ち主:共同管理者に法務の2人などを入れる(まだ入れていない)

本番への入れ方

月末・月初は稟議が多いので避けた。Webhook の URL はファイルにもチャットにも書かない。

  1. 指定ログイン情報を初回だけ入れる。URL は仮の値でメタデータに書き、入れたあと設定画面で本物に差し替える
  2. Apex とフローを入れる(テストは RunSpecifiedTests で上のテストクラスを指定。フローは無効のまま入る)。本番では、最初に検証だけ(変更なし)を流して、テスト2件が通ることを確かめた
  3. 設定画面でフローを有効にする
  4. 作業用の権限セットの割り当てを外す

2回目以降に Apex やフローを直すときは、2 だけ。1 を入れ直すと、URL が仮の値に戻って通知が止まる。通知先を変えるときは、設定画面で指定ログイン情報の URL だけ差し替える。

失敗したとき

  • ▶ 自動のやり直し:試験環境では、最初の失敗のあと、約15分・31分・61分・2時間の間隔で4回やり直した(どれも 301 で失敗)
  • ▶ 記録:やり直しでも失敗したものは、フローの実行記録(FlowInterview。状態が Error)に残る
  • ▶ 失敗のメール:フローのエラーメールの宛先(設定 → プロセスの自動化の設定)に届く。本番の宛先は未確認

未検証

  • ▶ 本番の承認者(役員)のプロファイルで、Apex クラスへのアクセスが要るか(試験は管理者が承認した)
  • ▶ 稟議の URL が本番で正しく開くか
  • ▶ 本物のメンションが届くか、ほかの人の画面でボタンの表示が変わるか
  • ▶ 上の「301 になった形」で試していないもの
📄 同じ会社で、前任者しか分からなかった TeamSpirit の稟議を引き継いだ話はこちらの記事で書いています。
📄 同じ会社で、Slack のボタンで Mac の管理者権限を承認する仕組みを、本番前に点検した話はこちらの記事で書いています。

📅 30分無料相談、受け付けています

「承認のあと、誰が動くかが決まっていない」「Salesforce と Slack をつなぎたいが、作った人がいなくなったあとが心配」——そういった段階からお気軽にどうぞ。業務の流れの聞き取りから、仕組みづくりと止め方の引き継ぎまで対応しています。

📅 30分無料相談を予約する →