「すみません、ルームに入れないんですが……」
この問い合わせが、期の開始直後に必ず来る。毎回。複数の会員制講座を運営している団体で、Chatworkを受講者のコミュニケーション基盤として使っている。講座ごとにグループチャットを作り、受講者を招待し、連絡や課題のやり取りをする。
問題は、この「招待」の作業だ。
俺が関わる前は、事務局のスタッフが全部手でやっていた。受講者名簿のスプレッドシートを見ながら、Chatworkのコンタクトから名前を1人ずつ探して、グループに追加する。100人規模の講座なら、100回その作業を繰り返す。
しかも講座は1つじゃない。複数の講座が並行して走っていて、それぞれに期がある。第3期が終われば第4期が始まる。受講者は入れ替わる。途中参加もあれば途中退会もある。上級コースに進む人もいる。
事務局のスタッフに聞いたら、期の切り替え時期は丸一日この作業に張り付くこともあるという。名前の表記揺れ——スプレッドシートでは「山田 太郎」だがChatworkでは「yamada taro」になっていたり、ニックネームで登録されていたり——で、そもそも誰が誰だかわからない。1人見つけるのに5分かかることもある。100人で丸一日。大げさじゃない。
そして招待漏れが起きる。3人、5人。「入れません」の問い合わせが来て、慌てて探して追加する。受講者にとっては講座初日にルームに入れない。印象が悪い。事務局にとっては、丸一日かけて丁寧にやったのにミスが出る。やるせない。
俺はたまたまこの団体の技術サポートをしていて、ある日この作業を横で見ていた。Chatworkの画面とスプレッドシートを交互に見ながら、名前をコピーして検索して、見つからなくて「この人どれだろう……」と呟いているのを見て、「いや、これはAPIでやるべきだろう」と思った。
この記事では、Chatwork API + GAS + Claude Codeの3つを組み合わせて、この運用を自動化した全工程を書く。講座名や団体名は伏せるが、構成は汎用的なので同じ悩みを持つ人はそのまま使えると思う。
自動化する前の運用
事務局スタッフの手作業フローはこうだった:
1. 受講者リストをスプレッドシートに入力(事務局が手入力)
2. Chatworkにグループチャットを作成(「第N期 〇〇講座」)
3. 受講者をChatworkのコンタクトから探して、1人ずつメンバー追加
4. 初回メッセージ(挨拶 + ルーム説明 + スケジュール)を投稿
5. 受講者名簿を別のスプレッドシートに転記(管理用)
問題は3番だ。
Chatworkのメンバー追加は、UIではコンタクト一覧から名前で検索 → チェック → 追加を1人ずつやる。100人いたら100回。
ここに「複数講座の同時進行」が重なるとカオスになる。A講座の第4期とB講座の第2期とCコースのアドバンスが同じ月に始まる。受講者リストが3つ。ルームが3つ。一部の受講者は複数の講座を受けているので、どのルームに入れてどのルームに入れないかの判断も必要になる。
事務局のスタッフは技術者ではない。スプレッドシートは使えるが、APIもGASも縁がない。Chatworkは「連絡ツール」として使っているだけで、管理機能をフルに使いこなしているわけでもない。そもそもChatworkをこの規模の講座運営のインフラとして使っている事例自体が珍しいと思う。Chatwork運営会社(kubell)も想定していない使い方かもしれない。
そこに俺が入った。
全体アーキテクチャ
自動化後の構成はこう:
[スプレッドシート] [Chatwork] [Claude Code]
│ │ │
│ GAS(名簿転記) │ │
├─────────────────→ │ │
│ │ GAS(ルーム作成) │
│ ←──────────────────────┤
│ │ GAS(メンバー追加) │
│ ←──────────────────────┤
│ │ GAS(初回MSG送信) │
│ ←──────────────────────┤
│ │ │
│ │ MCP(状態確認) │
│ ←─────────────────────→│
│ │ │
│ Claude Code Skill(/register でワンコマンド実行)
└──────────────────────────────────────────────┘
やっていることは単純だ:
- GAS: スプレッドシートの読み書き + Chatwork APIの実行
- MCP: Claude Codeからの状態確認・アドホック操作
- Claude Code Skill: 一連の処理をコマンド一発で実行
Step 1: 名簿転記の自動化(GAS)
受講者リストが入ったスプレッドシートから、管理用スプレッドシートに必要な情報を転記する。
function syncRoster() {
const source = SpreadsheetApp.openById(SOURCE_SHEET_ID);
const target = SpreadsheetApp.openById(TARGET_SHEET_ID);
const sourceSheet = source.getSheetByName('受講者一覧');
const targetSheet = target.getSheetByName('名簿');
// ヘッダー行をスキップして全データ取得
const data = sourceSheet.getDataRange().getValues();
const headers = data[0];
const rows = data.slice(1);
// 必要なカラムのインデックスを取得
const nameIdx = headers.indexOf('氏名');
const emailIdx = headers.indexOf('メールアドレス');
const cwIdIdx = headers.indexOf('ChatworkアカウントID');
// ターゲットに転記
const existingIds = getExistingIds(targetSheet);
let newCount = 0;
// 実運用ではsetValuesで一括書き込みする。ここでは流れを示すためにappendRowを使用
rows.forEach(row => {
const cwId = row[cwIdIdx];
if (cwId && !existingIds.includes(cwId)) {
targetSheet.appendRow([
row[nameIdx],
row[emailIdx],
cwId,
new Date(), // 転記日時
'未招待' // ステータス
]);
newCount++;
}
});
Logger.log(`新規転記: ${newCount}件`);
return newCount;
}
function getExistingIds(sheet) {
const data = sheet.getDataRange().getValues();
return data.slice(1).map(row => row[2]); // ChatworkアカウントID列
}
ポイントはChatworkアカウントIDをキーにしていること。名前は表記揺れがあるが、アカウントIDは一意だ。
ハマったこと:アカウントIDの取得
受講者のChatworkアカウントIDをどう取得するか。これが最初の壁だった。
Chatwork APIには「メールアドレスからアカウントIDを検索する」エンドポイントがない。名前で検索もできない。つまり、受講者のアカウントIDは事前に何らかの方法で取得しておく必要がある。
試した方法と結果:
| 方法 | 結果 |
|---|---|
| コンタクト一覧から名前で検索 | 表記揺れで精度50%以下 |
| ルーム参加者一覧から取得 | 既存ルームに入っている人のみ |
| 申込フォームにChatwork IDを入力してもらう | ◎ これが正解 |
結局、受講申込時にChatworkのアカウントIDを入力必須にした。Google Formに「ChatworkアカウントID」フィールドを追加し、入力ガイド(「マイページ→プロフィール→表示されるIDを入力」)を添えた。
これでアカウントIDの取得問題は解決した。ただ、入力ミス(全角数字、スペース混入、別のIDを入力)は一定数ある。バリデーションはGAS側でやる:
function validateChatworkId(id) {
// 数字のみ、6〜10桁
const cleaned = String(id).replace(/[^\d]/g, '');
if (cleaned.length < 6 || cleaned.length > 10) {
return { valid: false, reason: '桁数不正' };
}
return { valid: true, id: cleaned };
}
Step 2: ルーム作成とメンバー招待(GAS + Chatwork API)
名簿が揃ったら、ルームを作ってメンバーを招待する。
function createCourseRoom(courseName, term, memberIds) {
const roomName = `${courseName} 第${term}期`;
const description = `${courseName} 第${term}期の受講者グループです。`;
// ルーム作成
const roomId = createRoom(roomName, description, memberIds);
// 初回メッセージ送信
const welcomeMessage = buildWelcomeMessage(courseName, term);
postMessage(roomId, welcomeMessage);
// スプレッドシートのステータスを更新
updateMemberStatus(memberIds, '招待済み', roomId);
return roomId;
}
function createRoom(name, description, memberIds) {
const payload = {
name: name,
description: description,
members_admin_ids: [ADMIN_ACCOUNT_ID].join(','),
members_member_ids: memberIds.join(','),
icon_preset: 'group'
};
const options = {
method: 'post',
headers: { 'X-ChatWorkToken': CHATWORK_API_TOKEN },
payload: payload,
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(
'https://api.chatwork.com/v2/rooms',
options
);
const result = JSON.parse(response.getContentText());
if (response.getResponseCode() !== 200) {
throw new Error(`ルーム作成失敗: ${result.errors}`);
}
return result.room_id;
}
ハマったこと:レート制限
Chatwork APIには5分間に300リクエストのレート制限がある。100人のメンバーを1人ずつ追加すると100リクエスト。これだけなら問題ないが、名簿転記 → ルーム作成 → メンバー追加 → メッセージ送信を一気にやると、あっさり300に到達する。
最初にやったとき、78人目で 429 Too Many Requests が返ってきた。残りの22人は招待されなかった。
対策として、APIコール間にウェイトを入れた:
function rateLimitedFetch(url, options) {
const response = UrlFetchApp.fetch(url, options);
// レート制限ヘッダーを確認
const remaining = response.getHeaders()['x-ratelimit-remaining'];
if (remaining && parseInt(remaining) < 10) {
// 残り10リクエスト以下なら1秒待つ
Utilities.sleep(1000);
}
if (response.getResponseCode() === 429) {
// 429が返ったら30秒待ってリトライ
Utilities.sleep(30000);
return UrlFetchApp.fetch(url, options);
}
return response;
}
x-ratelimit-remaining ヘッダーを見て、残り枠が少なくなったら自動でスローダウンする。429が返ったら30秒待ってリトライ。これで100人でも安定して招待できるようになった。
ハマったこと:メンバー権限の罠
Chatwork APIの POST /rooms でメンバーを指定するとき、members_admin_ids と members_member_ids と members_readonly_ids の3種類がある。
最初、全員を members_admin_ids(管理者権限)にしてしまった。受講者全員が管理者。誰でもルーム名を変更でき、メンバーを追加・削除でき、ルームを削除できる。
翌日、ルーム名が「第4期 〇〇講座(改)」に変わっていた。受講者の誰かが試しにいじったらしい。慌てて全員を members_member_ids に変更した。
// ❌ やらかした
members_admin_ids: [ADMIN_ACCOUNT_ID, ...memberIds].join(','),
// ✅ 正しい
members_admin_ids: [ADMIN_ACCOUNT_ID].join(','),
members_member_ids: memberIds.join(','),
管理者は運営アカウントだけにする。当たり前のことだが、ドキュメントのサンプルコードが members_admin_ids しか使っていなかったので、何も考えずにコピペした。
Step 3: Claude Code Skillでワンコマンド化
ここまでのGASスクリプトを、Claude Codeから1コマンドで実行できるようにした。
Claude Codeには「Skill」という仕組みがある。/register と打つだけで、一連の処理が走る。
<!-- .claude/skills/register/SKILL.md -->
# /register — 受講生登録スキル
## 手順
1. GAS MCPで名簿転記スクリプトを実行
2. 転記結果を確認(新規N件、エラーM件)
3. エラーがあれば内容を表示して判断を仰ぐ
4. ルーム作成 + メンバー招待を実行
5. 招待結果をスプレッドシートに反映
6. 完了報告
GAS MCPを使っているので、Claude CodeからGASを直接実行できる。スプレッドシートの読み書きもGAS経由。Chatwork APIの実行もGAS経由。Claude Codeは「司令塔」として、各ステップの結果を見て次のアクションを判断する。
たとえば、名簿転記でバリデーションエラーが出たら:
[Claude Code] 名簿転記完了。新規98件、エラー2件。
エラー内容:
- 佐藤花子: ChatworkアカウントID「abc123」→ 数字のみではありません
- 田中一郎: ChatworkアカウントIDが空欄
→ エラーの2件をスキップしてルーム作成を進めますか?
それともエラーを修正してから再実行しますか?
こういう判断が必要な場面で、人間(自分)に確認を求める。全自動ではなく半自動。ここが大事だ。100人の招待を全自動でやって、エラーを無視してそのまま進むのは怖い。
GAS MCPの使い方
Claude CodeからGASを実行するには、GAS MCPを設定する。
{
"mcpServers": {
"gas-mcp": {
"command": "npx",
"args": ["-y", "gas-mcp"],
"env": {
"GAS_PROJECT_DIR": "/path/to/gas/project"
}
}
}
}
これでClaude Codeから exec ツールでGASの関数を直接実行できる。syncRoster() を呼べば名簿転記が走り、createCourseRoom() を呼べばルーム作成が走る。
注意点として、GAS MCPはclaspと連携している。GASプロジェクトはclasp cloneでローカルに落としておく必要がある。この初期設定が少し面倒だが、一度やれば以降はターミナルからGASを自在に操作できる。
Step 4: 運用フローの仕上げ
期の切り替え
期が変わるタイミングで、前期のルームを「読み取り専用」にして、新期のルームを作る。getRoomMembers や updateRoomMembers 等の共通関数は別途定義している前提で書く。
function archiveRoom(roomId) {
// 全メンバーを readonly に変更
const members = getRoomMembers(roomId);
const memberIds = members
.filter(m => m.account_id !== ADMIN_ACCOUNT_ID)
.map(m => m.account_id);
updateRoomMembers(roomId, {
members_admin_ids: [ADMIN_ACCOUNT_ID],
members_readonly_ids: memberIds
});
// ルーム名に [終了] を追加
updateRoom(roomId, {
name: `[終了] ${getRoomName(roomId)}`
});
// 最終メッセージ
postMessage(roomId,
'本期の講座は終了しました。このルームは閲覧専用になります。\nお疲れさまでした!'
);
}
途中参加者の追加
期の途中で受講者が追加になることがある。名簿を更新して /register を再実行すれば、差分だけ(新規の受講者だけ)が招待される。既存メンバーは getExistingIds でスキップされるので、重複招待にはならない。
退会者の処理
退会者はスプレッドシートの「ステータス」列を「退会」に変更し、GASで該当ルームからメンバーを削除する。ここも手動は怖いので、/remove スキルを作った。
結果
| 指標 | 事務局の手作業 | 自動化後 |
|---|---|---|
| 名簿転記(1講座あたり) | 30分〜1時間 | 10秒 |
| ルーム作成 + メンバー招待 | 2〜3時間(100人規模) | 2分 |
| 初回メッセージ送信 | 15分 | 自動 |
| 招待漏れ | ほぼ毎期 | 0件 |
| 複数講座が重なった月 | 丸一日以上 | 10分以下 |
数字だけ見ると「時間短縮」の話に見えるが、実は本質はそこじゃない。
事務局のスタッフが丸一日かけてやっていた作業が10分になった——それはそうなのだが、一番変わったのは「入れません」の問い合わせがゼロになったことだ。受講者にとっては講座初日にスムーズにルームに入れる。事務局にとっては、期の切り替え日が修羅場でなくなる。
技術者の自分には「APIで自動化すれば一瞬」だった。でも事務局にとっては「APIって何?」の世界だ。この距離を埋めるのが、たぶん一番価値のある仕事だった。
この構成の汎用性
講座運用に限らず、以下のような「定期的にChatworkルームを作ってメンバーを管理する」業務なら同じ構成で組める:
- プロジェクトの立ち上げ(メンバー招待 + 初回アナウンス)
- 部署異動時のルーム再編成
- イベント参加者のグループ作成
- 顧客別のサポートルーム作成
スプレッドシートが「誰を招待するか」のマスターデータ。GASがChatwork APIを叩く手足。Claude Codeが判断を挟む頭脳。この構成は応用が利く。
振り返って
自動化スクリプトの開発自体に丸1日かかっている。でもこの仕組みは1回作れば何年も使える。講座は年に何回もあるし、来年もある。丸一日の手作業が10分になるなら、開発に1日かけた元は初回で取れる。
それに、事務局のスタッフが「え、もう終わったんですか?」と言ったときの顔は、わりと忘れられない。
Chatworkを「チャットツール」として使っている会社は97万社あるらしい。でも、数百人規模の会員制講座のコミュニケーション基盤として、期ごとにルームを動的に生成・アーカイブし、名簿スプレッドシートとAPI連携で自動運用している事例は——たぶんほとんどない。少なくとも俺は見たことがない。
Chatworkにはこういう使い方もある、という話。よくできたツールだと思う、改めて。ただ、登録時に会社名を入れさせたりするのは主婦などにはやさしくないと、思っている。
Chatworkシリーズ
- #1 なぜ2026年にまだChatworkを使い倒しているのか
- #2 chatwork-client-gas、ぶっちゃけいるの?
- #3 ルームの参加者データだけで、組織の人間関係マップを作った
- #4 「Chatworkに確定連絡が来たら請求書を送る」をGASで自動化する
- #5 Chatwork MCPを繋いだら、17ルームの未読が10秒で片付いた
- #6 MCP vs GAS — Chatwork自動化の「正解」はどっちか
- #7 コンタクト承認をn8nで自動化しようとしたら、3つの罠にハマった
- #8 ChatworkにAIチームを住まわせたら、勝手に会話が始まった
- #9 Chatwork 8ルームの全メッセージからFAQ429件を自動抽出した
- #10 Webhook署名検証を入れたら全メッセージが消えた
- #11 過去メッセージを全件取得しようとしたら、APIの「100件の壁」にハマった
- #12 Chatwork APIの「既読」は自分で制御できる
- #13 Chatwork APIのファイル機能、使ったことある?
- #14 n8nで全ルーム巡回
- #15 タスク機能をAPIで使い倒す
- #16 MCPを2アカウント同時接続したら、仕事用と事務局用が1画面で回った
- #17 【世界初かもしれない】ChatworkでClaude Code Channelsを実装してみた
- #18 Chatwork × Dify × GASで問い合わせ回答を自動提案する
- #19 RelationMapを夜間バッチで毎日自動更新する
- #20 17記事書いて見えた、Chatwork APIエコシステムに足りないもの
- #21 Googleフォームの回答をChatworkに自動投稿するGAS
- #22 Chatworkの会話を毎日AIが要約してくれる仕組みをn8nで作った話
- #23 chatwork-cliを入れたら、シェルからChatworkが操作できて世界が変わった
- #24 ChatworkのWebhookをn8nで受けるなら、HMAC署名検証は必ずやれ
- #25 Chatwork × GAS × Claude Codeで会員制講座の運用を自動化した(この記事)