はじめに
テック企業のカスタマーサクセス(CS)や社内ヘルプデスクにおいて、「Slackで問い合わせを受ける」 ということがあると思います。
しかし、単純なSlack運用には限界があります。
- 「あの件どうなってる?」: スレッドが流れすぎて状況が追えない
- 「誰かやってると思った」: 担当者が曖昧なまま放置される
- 「転記するのが面倒」: 結果、エンジニアへのエスカレーションが遅れる
SlackからLinearへのチケット起票を自動化するだけでは、まだ半分です。
今回はさらに一歩進んで、「SlackとLinearを同期し、放置を許さないシステム」 を構築します。
実装する機能
- モーダル起票: フォーム入力で、ユーザーが意識的にLinearチケットを作成する
- コメント同期: その後のスレッド会話を、自動でLinearコメントに同期する
- 未対応通知: 放置されているチケットを毎朝通知する
0. 「公式インテグレーション」を使わない?
Linearには公式のSlack連携機能がありますが、あえて自作Botを作る理由は以下の3点。
-
コスト削減 (License Optimization):
公式連携で「Slackからコメント返信」をするには、そのユーザーのアカウント紐付け(=有料ライセンス)が必要になるケースが多いです。Bot経由なら、あまり利用しない問い合わせ側のライセンス費用をカットできます -
AIによる魔改造 (AI Enhancement):
公式機能は単なる「転記」ですが、自作なら途中に処理を挟めます。「エンジニア語の翻訳」「類似検知」「要約」といった付加価値は、公式機能では実現できません -
プロセスの強制 (Workflow Enforcement):
公式の/linearコマンドは自由度が高すぎます。「必ず期限を入れさせたい」「未対応チケを毎朝晒したい」といった、チーム固有の厳しい運用ルールをシステムで強制できるのが、自作の強みです
1. 意識的なチケット起票
スタンプ反応での自動化は手軽ですが、「とりあえず押すだけ」 になりがちで、後から詳細を聞き返すコストが発生します。
問い合わせる側に 「チケットを切る」という意識を持ってもらい、必要な情報を最初に入力してもらう ために、Slackのショートカット(モーダル)を使います。
def create_linear_issue(title, description, due_date=None, team_id="YOUR_TEAM_ID"):
query = """
mutation IssueCreate($title: String!, $description: String!, $teamId: String!, $dueDate: DateTime) {
issueCreate(input: { title: $title, description: $description, teamId: $teamId, dueDate: $dueDate }) {
issue { id url }
}
}
"""
# requests.post(...) の実装は省略
print(f"Creating issue: {title}")
return "ENG-123" # Mock ID
# モーダルを表示
@app.shortcut("create_ticket")
def open_modal(ack, body, client):
ack()
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "ticket_submission",
"title": {"type": "plain_text", "text": "問い合わせ作成"},
"submit": {"type": "plain_text", "text": "送信"},
"blocks": [
{
"type": "input",
"block_id": "title_block",
"label": {"type": "plain_text", "text": "件名"},
"element": {"type": "plain_text_input", "action_id": "title"}
},
{
"type": "input",
"block_id": "date_block",
"label": {"type": "plain_text", "text": "希望対応期日"},
"element": {"type": "datepicker", "action_id": "due_date"}
},
{
"type": "input",
"block_id": "desc_block",
"label": {"type": "plain_text", "text": "詳細"},
"element": {"type": "plain_text_input", "multiline": True, "action_id": "desc"}
}
]
}
)
# モーダル送信時の処理
@app.view("ticket_submission")
def handle_submission(ack, body, client, view):
ack()
user_id = body["user"]["id"]
title = view["state"]["values"]["title_block"]["title"]["value"]
due_date = view["state"]["values"]["date_block"]["due_date"]["selected_date"]
desc = view["state"]["values"]["desc_block"]["desc"]["value"]
# Linear APIでIssue作成
issue_id = create_linear_issue(
title=title,
description=f"Reporter: <@{user_id}>\nDue Date: {due_date}\n\n{desc}",
due_date=due_date
)
# 起票者にDMで通知
client.chat_postMessage(
channel=user_id,
text=f"チケットを作成しました!\n{issue_id}"
)
このひと手間で、「何が起きたのか」「どうしてほしいのか」 が明確な状態でチケットが作られます。
エンジニアが「詳細教えてください」と返す往復回数を減らすことができます。
2. Slack ↔ Linear コメント同期
問い合わせ側はSlackだけを見ている 状態を目指します。
実装コード (Python / Slack Bolt)
Slackの thread_ts はスレッドごとに一意の値になるのでLinearの issue_id の対応表を持つようにしてみます。
これが存在する場合のみ、Linearに同期すると実現できそう。Linearは GraphQL API が良さそうです。
import os
from slack_bolt import App
from slack_sdk import WebClient
import requests
SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
SLACK_SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"]
LINEAR_API_KEY = os.environ["LINEAR_API_KEY"]
app = App(token=SLACK_BOT_TOKEN, signing_secret=SLACK_SIGNING_SECRET)
# お試し用DBもどき
ticket_mapping = {
"1701234567.890123": "ENG-123"
}
def post_linear_comment(issue_id, text, user_name):
query = """
mutation CommentCreate($issueId: String!, $body: String!) {
commentCreate(input: { issueId: $issueId, body: $body }) {
success
}
}
"""
body = f"**{user_name} (from Slack)**:\n{text}"
requests.post(
"https://api.linear.app/graphql",
headers={"Authorization": LINEAR_API_KEY, "Content-Type": "application/json"},
json={"query": query, "variables": {"issueId": issue_id, "body": body}}
)
@app.event("message")
def handle_message_events(body, logger):
event = body.get("event", {})
# スレッドへの返信かどうか確認
thread_ts = event.get("thread_ts")
if not thread_ts:
return
# 過去にチケット化されたスレッドか確認
issue_id = ticket_mapping.get(thread_ts)
if not issue_id:
return
if event.get("bot_id"):
return
text = event.get("text")
user = event.get("user") # 実際はusers.info APIで名前を取得
# Linearに転送
post_linear_comment(issue_id, text, f"User({user})")
if __name__ == "__main__":
app.start(port=3000)
これで、Slackで「了解です、確認します」と返信するだけで、自動的にLinear側にもログが残ります。
3. 未対応チケットの「放置アラート」
人間は忘れる生き物ですしだれが対応するんだとなりがちです。システムにリマインドしてもらいましょう。
Linear APIを使い、「ステータスがInProgressでない」かつ「最終更新から24時間経過」 しているチケットを洗い出します。
実装コード (Python script)
このスクリプトをCronやCloud Schedulerで10:00に回してみます。
import os
import requests
from slack_sdk import WebClient
LINEAR_API_KEY = os.environ["LINEAR_API_KEY"]
SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
TARGET_CHANNEL = "#support-monitoring"
client = WebClient(token=SLACK_BOT_TOKEN)
def get_stale_issues():
query = """
query Issues {
issues(filter: {
state: { name: { eq: "Triage" } },
updatedAt: { lt: "2024-12-04T00:00:00Z" } # 実際は動的に日付生成
}) {
nodes {
title
url
assignee { name }
createdAt
}
}
}
"""
# ... (API request omission) ...
return response.json()["data"]["issues"]["nodes"]
def send_alert(issues):
if not issues:
return
text = "🚨 **未対応の問い合わせがあります!** 🚨\n\n"
for issue in issues:
assignee = issue['assignee']['name'] if issue['assignee'] else "Unassigned"
text += f"• <{issue['url']}|{issue['title']}> (担当: {assignee})\n"
client.chat_postMessage(channel=TARGET_CHANNEL, text=text)
if __name__ == "__main__":
# 日付計算ロジックなどは省略
issues = get_stale_issues()
if issues:
send_alert(issues)
4. GCPで手軽に作るアーキテクチャ
コスト面も考慮しないといけませんね。常時起動のサーバーは不要です。Google Cloud (GCP) のCloud Runで運用を試みます。
-
Cloud Run / Cloud Functions (Gen 2):
Pythonコード (main.py) を置く場所。Slackからのイベントフックと、毎朝の定期実行リクエストの両方を処理します -
Firestore:
マッピングDB (ticket_mapping) の正体。何でも良いですがFirestoreとします。
キーバリューストア感覚でthread_tsとissue_idを保存するのに最適です。無料枠も広いため、小規模なら無料でいけるかも? -
Cloud Scheduler:
毎朝9時に Cloud Functions の特定エンドポイント (/daily-reminder) を叩くだけのジョブを設定します
これで、「サーバー管理ゼロ、もしかしたらコストゼロ」 の基盤の完成です。
5. AIでさらに進化させるアイデア
ここまで組めれば、あとはパーツに「AI」を組み込むだけで、さらに高度な自動化が可能になります。(geminiにも聞いてみた)
A. 「エンジニア語」を「顧客語」に翻訳(Auto-Reply)
エンジニアがLinearに書き込んだ技術的な調査結果(例:「DBのインデックスが効いてなかった。修正済み」)を、そのまま伝えるのは危険です。
AIに 「この内容を、非エンジニアのお客様にも伝わる丁寧な表現に書き換えて」 と指示し、Slackの下書きとして投稿させることで、問い合わせ担当者の負担を軽減させられます。
B. 類似チケットの自動検知(De-duplication)
問い合わせが来た「瞬間」に、過去のLinearチケットやNotionのFAQから 「似たような事象」 をベクトル検索します。
def find_similar_issue(new_text):
# 1. 新しい問い合わせをベクトル化
response = client.embeddings.create(
input=new_text,
model="text-embedding-3-small"
)
new_vector = response.data[0].embedding
# 2. Vector DB (Pinecone / Supabaseなど) から類似検索
# 事前にLinearの全Issueをベクトル化して入れておく必要がありますが
results = vector_db.query(vector=new_vector, top_k=1)
if results and results[0].score > 0.85:
return results[0].metadata["url"]
return None
これをモーダル送信時の処理に挟み込みます。
「恐れ入りますが、こちらのチケット(リンク)と同じ事象ではありませんか?」 などBotが先回りして聞くことで、
人間が調査する時間をゼロにし、チケットの重複乱立も防げます。
似た問い合わせって運用でカバーしがちですがこれなら漏れなくサポートできそうですね。
C. 長文スレッドの要約(Summarization)
コメント同期するとき、会話をそのまま転記するのではなく、AIに 「要点だけを3行でまとめて」 Linearに同期させます。
これでエンジニアは、読む時間を大幅に短縮できます。
急いでいたり想定外の事象だったりすると日本語が難しかったりしますからね。
6. 本当にCS担当者にLinearアカウントは必要?
日常業務においては「不要」にできます。
このシステムのポイントは、Botが「プロキシ(代理人)」なることです。
Linear上では、全てのコメントは「Botユーザー(API Keyの持ち主)」によって投稿されますが、本文に **User Name (from Slack)** と記載されるため、誰の発言かは分かります。
しかしたまに状況を確認したいよねっていうケースや日常的に非エンジニア組織が確認を行いたいケースが発生した場合共通アカウントで対応するのも微妙だったりします。そういったときは素直にアカウントを発行するのが無難かなあと思うところです。
まとめ
自前版Slack × Linear 同期を作りました。
- 発生: Slackに問い合わせが来る → 自動でLinearチケット化
- 会話: Slackでやり取りする → 自動でLinearに同期
- 解決: Linearで完了にする
- 放置: 毎朝Slackでリマインド
毎日欠かさず!インティメート・マージャー アドベントカレンダーを見てください!
また、インティメート・マージャーでは、新卒から中途採用まで幅広く採用募集中です!
記事を読んで弊社に興味を持ってくれた方は、下記より採用情報をチェック!