Use Cases (更新: 2026/6/7)

Slack BotをNodeで作る:Boltでスラッシュコマンド・通知・署名検証まで通す

Bolt(Node)でSlack Botを作る手順。権限スコープ、イベント購読、スラッシュコマンド、通知、署名検証までコピペで動くコード付きで解説。

Slack BotをNodeで作る:Boltでスラッシュコマンド・通知・署名検証まで通す

初めて作ったSlack Botは、テストでは完璧でした。/triage add ログイン直して と打つと、ちゃんと専用チャンネルにカードが飛ぶ。同僚にも見せて「便利じゃん」と言われて、いい気分でした。

ところが本番チャンネルに出した翌週、Slackから一通のメールが来ました。「あなたのアプリへのリクエストが失敗しています」。ログを見ると、スラッシュコマンドのたびに operation_timeout が並んでいる。原因は単純で、僕がコマンドを受けた瞬間に外部APIを叩いて、3秒以内に返事をしていなかったからでした。

Slackには「とにかく3秒以内に受け取ったよと返せ」という独特のルールがあります。重い処理はそのあとでいい。それを知らずに、僕は受信と処理を一緒にやって自爆していたわけです。

この記事では、その手の事故を踏まないSlack Botの作り方を、Bolt for JavaScript(Node.js)で最初から最後まで通します。アプリ作成と権限スコープ、イベント購読、スラッシュコマンド、通知の送り方、そして署名検証。コードはコピペでそのまま動く形で置きます。Discord Botを作りたい人は、3秒ルールやトークン管理が別物なので、専用のdiscord.jsでDiscord Botを作る記事へどうぞ。

この記事の要点

  • Slack Botの土台は Bolt for JavaScript。Slackから来たイベントを「どの関数に渡すか」を整理する公式フレームワークです。
  • スラッシュコマンドとボタンとモーダルは、まず ack() で3秒以内に受信確認を返す。重い処理はそのあと。これを破ると本番でタイムアウトします。
  • 権限(scope)は最小から。commandschat:writeapp_mentions:read の3つで、トリアージbotは動きます。
  • ローカル検証は Socket Mode(公開URL不要)、本番は Request URL(HTTPS)。混ぜないのがコツ。
  • HTTPで受けるなら 署名検証 が必須。Boltが肩代わりしてくれますが、仕組みは自分の手で1回書いておくと安心です(後半にコピペ可能なコードを置きます)。

通知botで終わらせない

Slack Botは、メッセージ・スラッシュコマンド・ボタン・モーダル入力に反応して業務を進めるアプリです。Bolt for JavaScriptは、その受け口をNode.jsで書くための公式フレームワーク。要するに「Slackから来たイベントを、どの関数に渡すか」を整理する足場です。

ありがちな失敗は、通知を投げるだけの小さなコードで満足してしまうこと。僕の最初のbotがまさにそれでした。チャンネルに「問い合わせ来たよ」とテキストを1行流すだけ。便利は便利でしたが、担当者も緊急度も完了状態も残らない。結局、誰かが処理したのか分からなくて、チャンネルを上から読み返すハメになりました。通知botは、情報を流すだけで構造化しないから、すぐ限界が来ます。

本当に役立つbotは、入力を受けたら「件名・緊急度・依頼者・状態」をきちんと残します。問い合わせ受付、障害の一次対応、日次レポート、公開前チェック。どれも「Slack上のやりとりを、あとで追える形に整える」のが本体です。今回はその例として、問い合わせをさばく トリアージbot を作ります。

先に決めるユースケース

いきなり「Slack Botを作って」と手を動かすと、だいたい薄いサンプルで終わります。先に決めるべきは4つだけ。入口(どこから来るか)・保存する項目・返信の形・失敗したときの扱いです。

ユースケースSlack上の入口botがやること注意点
問い合わせトリアージ/triage add、モーダル件名・緊急度・依頼者をそろえて専用チャンネルに投稿顧客名・秘密情報・未公開URLを貼らせない
障害一次対応@bot メンション、ボタン初動チェックリストを返し、担当者とスレッドを残す断定回答しすぎず、人間へ渡す条件を決める
日次レポート/triage list、定時ジョブ未完了項目をまとめて朝会や日報に貼る件数が多いとSlackの長文制限に当たる
公開前チェックスラッシュコマンドCTA・内部リンク・担当者・公開URLを確認下書きURLと本番URLを混ぜない

この記事で作るbotの流れは、こうです。

flowchart LR
  A["Slackユーザー"] --> B["/triage または @メンション"]
  B --> C["Boltのlistener"]
  C --> D["トリアージのロジック"]
  D --> E["chat.postMessageで通知"]
  D --> F["モーダルとボタン"]

設計が決まったら、Claude Codeに渡す依頼文も具体的になります。「Slack Bot作って」ではなく、ここまで噛み砕いて渡すと、出てくるコードの精度がまるで違います。

Bolt for JavaScriptでSlack Botを実装してください。
目的は問い合わせトリアージです。
含めるもの:
- Socket ModeとRequest URLを環境変数で切り替える
- /triage add, /triage list, /triage modal
- モーダル入力とview_submission
- Mark doneボタン
- app_mentionへの案内返信
- scopes、secrets、署名検証の説明
- triage.tsの単体テスト
疑似APIは使わず、コピペで動くTypeScriptにしてください。

Socket ModeとRequest URLの選び方

Slackがイベントを届ける口は2種類あります。最初にここでつまずく人が多いので、表で整理します。

Socket Mode は、公開HTTPSエンドポイントを用意せず、bot側からSlackへWebSocketでつなぎに行く方式です。ローカル開発や社内PoCに向いていて、xapp- で始まるApp-Level Tokenを使います。自宅PCで動かしても外からURLを叩かれないので、最初の検証はこれが楽です。

Request URL は、Slackから自分のHTTPSエンドポイントへPOSTしてもらう方式です。本番運用ではこちらが基本。HTTPで受けるぶん、リクエストが本当にSlackから来たかを Signing Secret で検証します。

方式向いている場面必要なもの落とし穴
Socket Modeローカル開発、社内PoCSLACK_APP_TOKENconnections:writeプロセスが落ちると受信できない。Marketplace配布には不向き
Request URL本番HTTP運用HTTPS URL、SLACK_SIGNING_SECRETack() が遅いとSlack側でタイムアウト

おすすめの順番は、まずSocket Modeで動作確認 → 外部ユーザーや本番チャンネルに出す段階でRequest URLへ移す、です。後で出すコードは SLACK_SOCKET_MODE=true の1行で切り替えられるようにしてあります。

アプリ作成と権限スコープ

Slackアプリは Slack APIのアプリ管理画面 から作ります。GUIでポチポチ設定してもいいのですが、僕は manifest(設定をまとめたYAML)をリポジトリに置く やり方を勧めます。開発用と本番用で差分がGitに残るので、「いつの間にか権限が増えてた」を防げます。

権限スコープは最小から始めるのが鉄則です。今回必要なのは3つだけ。commands はスラッシュコマンドを受けるため、chat:write はメッセージを投稿するため、app_mentions:read はbotへのメンションを受けるためです。

display_information:
  name: Claude Triage Bot
  description: Slackから問い合わせを集めるbot
  background_color: "#2E2A24"
features:
  bot_user:
    display_name: Claude Triage
    always_online: false
  slash_commands:
    - command: /triage
      description: トリアージ項目を追加・一覧表示する
      usage_hint: "add ログイン直して | list | modal"
      should_escape: true
oauth_config:
  scopes:
    bot:
      - app_mentions:read
      - chat:write
      - commands
settings:
  event_subscriptions:
    bot_events:
      - app_mention
  interactivity:
    is_enabled: true
  socket_mode_enabled: true
  org_deploy_enabled: false
  token_rotation_enabled: false

channels:historygroups:history(チャンネルの過去ログを読む権限)は最初から足しません。botが本当に履歴を読む設計になった時点で、プライバシーと監査を含めてレビューします。権限は「足すのは簡単、外すのは怖い」ので、最初は削れるだけ削るのが安全です。

event_subscriptionsapp_mentionイベント購読 の指定です。これがあって初めて、botは「自分が呼ばれた」というイベントを受け取れます。interactivity を有効にしないと、ボタンやモーダルのpayloadが届かないので、ここも忘れずに。

ローカルプロジェクトを作る

Node.js 20以上を前提にします。以下はそのまま貼って試せる最小構成です。

mkdir claude-slack-triage-bot
cd claude-slack-triage-bot
npm init -y
npm install @slack/bolt @slack/types dotenv
npm install -D typescript tsx vitest @types/node
npm pkg set type=module
npm pkg set scripts.dev="tsx watch src/app.ts"
npm pkg set scripts.build="tsc"
npm pkg set scripts.start="node dist/app.js"
npm pkg set scripts.test="vitest run"
mkdir src tests

TypeScriptの設定はこれだけあれば動きます。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}

トークン類は .env.example で名前だけ共有し、実値は .env やホスティング側のsecret managerに入れます。Gitにも記事にも絶対に載せません。

SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_SOCKET_MODE=true
SLACK_APP_TOKEN=xapp-your-app-level-token
TRIAGE_CHANNEL_ID=C0123456789
PORT=3000

xoxb- がbot token、xapp- がSocket Modeで使うApp-Level Token、Signing SecretはリクエストがSlack由来かを確かめる鍵です。トークンの種類でハマったら、Slackのtokens解説を読むと早いです。秘密の扱い全般はAPIキーを漏らさない手順にまとめてあります。

コピペで動くBolt実装

設計のコツは、Slackに依存しないロジックを先に分ける ことです。src/triage.ts に純粋な関数だけを置くと、テストが書きやすく、Bolt側が薄くなります。

// src/triage.ts
import type { KnownBlock, View } from "@slack/types";

export type Severity = "low" | "normal" | "high";

export interface Ticket {
  id: string;
  channelId: string;
  title: string;
  createdBy: string;
  severity: Severity;
  status: "open" | "done";
  createdAt: string;
}

const tickets = new Map<string, Ticket>();

export function resetForTest() {
  tickets.clear();
}

// "add ログイン直して" を { action, title } に分解する
export function parseTriageText(text: string) {
  const [actionRaw, ...rest] = text.trim().split(/\s+/);
  return { action: actionRaw || "help", title: rest.join(" ").trim() };
}

export function addTicket(input: {
  channelId: string;
  title: string;
  createdBy: string;
  severity?: Severity;
}) {
  const ticket: Ticket = {
    id: `triage_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
    channelId: input.channelId,
    title: input.title,
    createdBy: input.createdBy,
    severity: input.severity ?? "normal",
    status: "open",
    createdAt: new Date().toISOString(),
  };
  tickets.set(ticket.id, ticket);
  return ticket;
}

export function completeTicket(id: string) {
  const ticket = tickets.get(id);
  if (!ticket) return undefined;
  const updated: Ticket = { ...ticket, status: "done" };
  tickets.set(id, updated);
  return updated;
}

// 指定チャンネルの未完了チケットだけを一覧テキストにする
export function formatTicketList(channelId: string) {
  const open = [...tickets.values()].filter((ticket) => {
    return ticket.channelId === channelId && ticket.status === "open";
  });

  if (open.length === 0) return "未完了のトリアージ項目はありません。";

  return open
    .map((ticket, index) => {
      return `${index + 1}. [${ticket.severity}] ${ticket.title} by <@${ticket.createdBy}>`;
    })
    .join("\n");
}

// チケットを「カード + Mark doneボタン」のBlock Kitに変換する
export function ticketBlocks(ticket: Ticket): KnownBlock[] {
  return [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*${ticket.title}*\nSeverity: ${ticket.severity}\nOwner: <@${ticket.createdBy}>`,
      },
    },
    {
      type: "actions",
      elements: [
        {
          type: "button",
          text: { type: "plain_text", text: "Mark done" },
          action_id: "triage_done",
          value: ticket.id,
        },
      ],
    },
  ];
}

export function modalView(): View {
  return {
    type: "modal",
    callback_id: "triage_modal_submit",
    title: { type: "plain_text", text: "New triage" },
    submit: { type: "plain_text", text: "Create" },
    close: { type: "plain_text", text: "Cancel" },
    blocks: [
      {
        type: "input",
        block_id: "title_block",
        label: { type: "plain_text", text: "対応が必要なことは?" },
        element: {
          type: "plain_text_input",
          action_id: "title_input",
          min_length: 3,
          max_length: 120,
        },
      },
      {
        type: "input",
        block_id: "severity_block",
        label: { type: "plain_text", text: "緊急度" },
        element: {
          type: "static_select",
          action_id: "severity_input",
          initial_option: {
            text: { type: "plain_text", text: "Normal" },
            value: "normal",
          },
          options: [
            { text: { type: "plain_text", text: "High" }, value: "high" },
            { text: { type: "plain_text", text: "Normal" }, value: "normal" },
            { text: { type: "plain_text", text: "Low" }, value: "low" },
          ],
        },
      },
    ],
  };
}

次にBoltのlistenerをつなぎます。listenerは、スラッシュコマンド・モーダル送信・ボタン操作・メンションを受ける関数です。冒頭の事故を思い出してください。どのlistenerも、最初に ack() を呼んでいる のが肝です。

// src/app.ts
import "dotenv/config";
import { App, LogLevel } from "@slack/bolt";
import {
  addTicket,
  completeTicket,
  formatTicketList,
  modalView,
  parseTriageText,
  ticketBlocks,
  type Severity,
} from "./triage.js";

const socketMode = process.env.SLACK_SOCKET_MODE === "true";
const required = ["SLACK_BOT_TOKEN", socketMode ? "SLACK_APP_TOKEN" : "SLACK_SIGNING_SECRET"];

// 起動時に必要な環境変数が無ければ、その場で止める(門番)
for (const key of required) {
  if (!process.env[key]) throw new Error(`環境変数がありません: ${key}`);
}

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode,
  appToken: process.env.SLACK_APP_TOKEN,
  logLevel: LogLevel.INFO,
});

app.command("/triage", async ({ ack, command, respond, client }) => {
  await ack(); // ← まず3秒以内に受信確認。重い処理はこのあと
  const parsed = parseTriageText(command.text);

  if (parsed.action === "add") {
    if (!parsed.title) {
      await respond("使い方: `/triage add ログインのリダイレクトを直す`");
      return;
    }

    const ticket = addTicket({
      channelId: command.channel_id,
      title: parsed.title,
      createdBy: command.user_id,
      severity: "normal",
    });

    await respond({
      response_type: "in_channel",
      text: `トリアージ項目を追加: ${ticket.title}`,
      blocks: ticketBlocks(ticket),
    });
    return;
  }

  if (parsed.action === "list") {
    await respond({ response_type: "ephemeral", text: formatTicketList(command.channel_id) });
    return;
  }

  if (parsed.action === "modal") {
    await client.views.open({ trigger_id: command.trigger_id, view: modalView() });
    return;
  }

  await respond("使い方: `/triage add ...`、`/triage list`、`/triage modal`");
});

app.view("triage_modal_submit", async ({ ack, view, body, client }) => {
  const titleState = view.state.values.title_block.title_input;
  const severityState = view.state.values.severity_block.severity_input;
  const title = titleState.type === "plain_text_input" ? titleState.value?.trim() : "";
  const severity =
    severityState.type === "static_select"
      ? severityState.selected_option?.value ?? "normal"
      : "normal";

  if (!title) {
    await ack({ response_action: "errors", errors: { title_block: "タイトルを入力してください。" } });
    return;
  }

  await ack();

  const channelId = process.env.TRIAGE_CHANNEL_ID ?? "modal-only";
  const ticket = addTicket({
    channelId,
    title,
    createdBy: body.user.id,
    severity: severity as Severity,
  });

  // 専用チャンネルが設定されていれば、そこへ通知を送る
  if (process.env.TRIAGE_CHANNEL_ID) {
    await client.chat.postMessage({
      channel: process.env.TRIAGE_CHANNEL_ID,
      text: `新しいトリアージ項目: ${ticket.title}`,
      blocks: ticketBlocks(ticket),
    });
  }
});

app.action("triage_done", async ({ ack, action, respond }) => {
  await ack();
  const value = action.type === "button" ? action.value : undefined;
  if (!value) return;

  const ticket = completeTicket(value);
  await respond(ticket ? `完了: ${ticket.title}` : "チケットが見つかりません。");
});

app.event("app_mention", async ({ event, say }) => {
  await say({
    thread_ts: event.ts,
    text: "`/triage add ...`、`/triage list`、`/triage modal` が使えます。",
  });
});

const port = Number(process.env.PORT ?? 3000);
if (socketMode) {
  await app.start();
} else {
  await app.start(port);
}

app.logger.info(`Slack botを起動: ${socketMode ? "Socket Mode" : `HTTPモード ポート${port}`}`);

最後に、Slackにつながずに動かせる単体テストを置きます。ここが「純粋なロジックを分けた」ご褒美です。

// tests/triage.test.ts
import { beforeEach, describe, expect, it } from "vitest";
import {
  addTicket,
  completeTicket,
  formatTicketList,
  parseTriageText,
  resetForTest,
} from "../src/triage";

describe("triage helpers", () => {
  beforeEach(() => resetForTest());

  it("スラッシュコマンドのテキストを分解する", () => {
    expect(parseTriageText("add Fix login")).toEqual({
      action: "add",
      title: "Fix login",
    });
  });

  it("未完了チケットだけを一覧する", () => {
    const ticket = addTicket({
      channelId: "C123",
      title: "料金ページのCTAを見直す",
      createdBy: "U123",
      severity: "high",
    });

    expect(formatTicketList("C123")).toContain("[high] 料金ページのCTAを見直す");
    completeTicket(ticket.id);
    expect(formatTicketList("C123")).toBe("未完了のトリアージ項目はありません。");
  });
});

実行はこの順番です。

npm run test
npm run build
npm run dev

Socket Modeなら npm run dev のままSlackで /triage add Slackから試す を打てば動きます。Request URLなら、デプロイ先の https://example.com/slack/events を、スラッシュコマンド・Interactivity・Event Subscriptionsの3か所に設定します。

署名検証を自分の手で書いてみる

Boltを使うと署名検証は自動でやってくれます。でも「中で何が起きているか」を1回は自分の手で書いておくと、本番でトラブったときに迷いません。生のExpressや別フレームワークで受けるときにも、この知識がそのまま効きます。

Slackは、リクエストヘッダに X-Slack-Signature(署名)と X-Slack-Request-Timestamp(送信時刻)を載せてきます。検証は、バージョン文字列 v0 ・タイムスタンプ・リクエスト本文 をコロンでつないだ文字列を、Signing SecretでHMAC-SHA256にかけ、ヘッダの署名と一致するかを比べるだけです。古い時刻のリクエストはリプレイ攻撃の疑いがあるので弾きます。

ポイントは2つ。検証には 加工前の生ボディ(raw body) が要ること、そして比較は タイミング攻撃に強い timingSafeEqual を使うことです。次のコードは、Node標準モジュールだけで動きます。

// src/verify-slack.ts
import { createHmac, timingSafeEqual } from "node:crypto";

// Slackからのリクエストか検証する。trueなら本物
export function isValidSlackRequest(args: {
  signingSecret: string;
  signature: string | undefined; // X-Slack-Signature ヘッダ
  timestamp: string | undefined; // X-Slack-Request-Timestamp ヘッダ
  rawBody: string; // ボディは必ず「加工前の生文字列」を渡す
}): boolean {
  const { signingSecret, signature, timestamp, rawBody } = args;
  if (!signature || !timestamp) return false;

  // リプレイ攻撃対策: 5分以上ズレた時刻は拒否する
  const fiveMinutes = 60 * 5;
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > fiveMinutes) return false;

  // v0:タイムスタンプ:生ボディ をコロンで連結して署名のもとを作る
  const base = `v0:${timestamp}:${rawBody}`;
  const mySignature = `v0=${createHmac("sha256", signingSecret).update(base).digest("hex")}`;

  // 長さが違うと timingSafeEqual が例外を投げるので先にそろえる
  const a = Buffer.from(mySignature, "utf8");
  const b = Buffer.from(signature, "utf8");
  if (a.length !== b.length) return false;

  // タイミング攻撃に強い比較で突き合わせる
  return timingSafeEqual(a, b);
}

実運用ではBoltに任せて構いません。ただ「署名が合わない」エラーの9割は、ボディをJSONパース済みのオブジェクトで渡してraw bodyを失っているケースです。仕組みを知っていれば、ここで一発で気づけます。署名検証のさらに詳しい考え方はWebhook受信の署名検証・冪等性の記事も合わせてどうぞ。元ネタはSlack公式のリクエスト検証ドキュメントです。

通知の送り方とよくある落とし穴

通知は chat.postMessage で送ります。text だけでも届きますが、Block Kit(先ほどの ticketBlocks)を使うと、見出し・本文・ボタンを持った「カード」になります。通知をクリックで完了にできるのは、このボタンのおかげです。

運用で踏みやすい穴を、僕がやらかした順に並べます。

  • ack() を遅らせない。 スラッシュコマンド・ボタン・モーダル送信では、DB更新や外部APIより先に受信確認を返す。冒頭の事故はこれです。
  • trigger_id を長く持たない。 /triage modal を受けたら先に views.open を呼ぶ。trigger_id は短命で、握ったまま重い処理をすると「モーダルが開かない」になります。
  • 権限不足をコードで直そうとしない。 chat:write 不足・bot未招待・app_mention 未購読は、ぜんぶSlack側の設定です。コードをいくらいじっても直りません。
  • Socket ModeとRequest URLを混ぜない。 起動ログに今どっちで動いているかを出すだけで、切り分けが一気に速くなります。
  • secretを露出しない。 xoxb-xapp-・Signing Secretを、プロンプト・スクショ・ログ・テストのfixtureに貼らない。漏れたら即ローテーション。
  • botに判断させすぎない。 障害や問い合わせでは、原因を断定するより「次に見る情報」と「人間へ渡す条件」を返すほうが、現場で信頼されます。

よくある質問

Q. Socket ModeとRequest URL、結局どっちを使えばいい? A. ローカルや社内検証はSocket Modeが楽です。公開URLが要らないので、自宅PCでもすぐ動きます。外部ユーザーや本番チャンネルに出す段階で、HTTPSのRequest URLへ移すのが定番の流れです。

Q. 「dispatch_failed」「operation_timeout」が出ます。 A. ほぼ ack() の遅延です。スラッシュコマンドやボタンを受けたら、何よりも先に await ack() を呼んでください。重い処理はそのあとに回します。

Q. スラッシュコマンドが入力欄に出てきません。 A. manifestに slash_commands を登録してアプリを再インストールしたか、botを対象チャンネルへ招待したかを確認します。コマンド名が既存アプリと衝突していないかもチェックを。

Q. 署名検証は自分で書くべき? A. Boltを使うなら基本は自動なので不要です。ただ生のExpressなどで受ける場合は自分で実装します。その際はraw bodyを失わないこと、timingSafeEqual で比較することの2点が要です。

Q. DiscordとSlack、両方で同じbotを動かせる? A. ロジック(今回の triage.ts)は共通化できますが、入口の作法はかなり違います。Discordには3秒ルールやGateway Intentsという別の決まりがあるので、Discord Botの作り方を別途参照してください。

実際に試した結果

実際に手を動かして分かったのは、最短ルートは「大きなSlack Botを一気に作る」ことではなかった ということです。

うまくいった順番はいつも同じでした。まずmanifestとscopeを固定する。次に triage.ts みたいなSlackに依存しない純粋なロジックを書いてテストを通す。最後にBoltのlistenerとSlack管理画面をつなぐ。この順番だと、設定ミスとコードのバグが混ざらないので、どこで詰まっても切り分けが速いんです。

Claude Codeに任せるときも同じで、コード生成だけ丸投げするより、権限・secret・テスト・公開前チェックを「同じ作業単位」で持たせると安定しました。冒頭のタイムアウト事故以来、僕がまず確認するのは「ack() が一番上にあるか」です。たったそれだけで、本番のエラーログはぴたりと止まりました。賢いbotを目指す前に、転んでもケガしない受け口を先に作る。これが結局いちばん速い、というのが今の実感です。

社内bot・Webhook・API・secrets管理の型をまとめて整えたいときは、研修・相談教材・テンプレートも役立つはずです。

#Slack Bot #Bolt JS #スラッシュコマンド #Slack 通知 #Node.js
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。